diff --git a/.vscode/settings.json b/.vscode/settings.json index e24e1d51..bd9de073 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { - "editor.formatOnSave": true, - "typescript.tsdk": "node_modules\\typescript\\lib", - "files.eol": "\n" -} \ No newline at end of file + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "typescript.tsdk": "node_modules\\typescript\\lib", + "files.eol": "\n" +} diff --git a/src/app/storybook/schema-editor/page.tsx b/src/app/storybook/schema-editor/page.tsx new file mode 100644 index 00000000..2c874690 --- /dev/null +++ b/src/app/storybook/schema-editor/page.tsx @@ -0,0 +1,37 @@ +"use client"; +import SchemaEditor from "@/components/gui/schema-editor"; +import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import { useState } from "react"; + +export default function SchemaEditorStorybook() { + const [value, setValue] = useState({ + name: { + old: "", + new: "", + }, + columns: [], + constraints: [], + createScript: "", + }); + + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/combobox.tsx b/src/components/combobox.tsx new file mode 100644 index 00000000..d231de3d --- /dev/null +++ b/src/components/combobox.tsx @@ -0,0 +1,60 @@ +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useState } from "react"; +import { Button } from "./ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "./ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +interface ComboboxProps { + value?: string; + onChange: (value: string) => void; + items?: { value: string; text: string }[]; +} + +export default function Combobox({ value, items, onChange }: ComboboxProps) { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + No column found. + + {(items ?? []).map((item) => ( + { + onChange(item.value); + setOpen(false); + }} + > + + {item.text} + + ))} + + + + + ); +} diff --git a/src/components/common-dialog/index.tsx b/src/components/common-dialog/index.tsx index cd851190..cfba91ef 100644 --- a/src/components/common-dialog/index.tsx +++ b/src/components/common-dialog/index.tsx @@ -1,5 +1,7 @@ "use client"; +import { Icon } from "@phosphor-icons/react"; import { noop } from "lodash"; +import { Loader } from "lucide-react"; import { PropsWithChildren, ReactElement, @@ -8,6 +10,8 @@ import { useContext, useState, } from "react"; +import CodePreview from "../gui/code-preview"; +import { Button } from "../ui/button"; import { Dialog, DialogContent, @@ -16,10 +20,6 @@ import { DialogHeader, DialogTitle, } from "../ui/dialog"; -import { Button } from "../ui/button"; -import { Icon } from "@phosphor-icons/react"; -import { Loader } from "lucide-react"; -import CodePreview from "../gui/code-preview"; interface ShowDialogProps { title: string; @@ -55,8 +55,13 @@ export function CommonDialogProvider({ children }: PropsWithChildren) { setDialogOption(null); }, []); + const showDialog = useCallback((option: ShowDialogProps) => { + setDialogOption(option); + setErrorMessage(""); + }, []); + return ( - + {children} {dialogOption && ( -
+
{display || "EMPTY STRING"}
diff --git a/src/components/gui/schema-editor/column-pk-popup.tsx b/src/components/gui/schema-editor/column-pk-popup.tsx index 46b6670c..dcf7515c 100644 --- a/src/components/gui/schema-editor/column-pk-popup.tsx +++ b/src/components/gui/schema-editor/column-pk-popup.tsx @@ -1,85 +1,69 @@ -import { DatabaseTableColumnConstraint, SqlOrder } from "@/drivers/base-driver"; -import { LucideKeyRound } from "lucide-react"; -import { Button } from "../../ui/button"; -import ConflictClauseOptions from "./column-conflict-clause"; -import { ColumnChangeEvent } from "./schema-editor-column-list"; -import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select"; + DatabaseTableColumnChange, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { produce } from "immer"; +import { LucideKeyRound } from "lucide-react"; +import { Dispatch, SetStateAction, useCallback } from "react"; export default function ColumnPrimaryKeyPopup({ - constraint, + schema, + column, disabled, onChange, }: Readonly<{ - constraint: DatabaseTableColumnConstraint; + schema: DatabaseTableSchemaChange; + column: DatabaseTableColumnChange; disabled: boolean; - onChange: ColumnChangeEvent; + onChange: Dispatch>; }>) { + const columnName = column.new?.name; + + // Check if the column is primary key + const isPrimaryKey = schema.constraints.some((constraint) => + ( + constraint.new?.primaryColumns ?? + constraint.old?.primaryColumns ?? + [] + ).includes(column.new?.name ?? column.old?.name ?? "") + ); + + const removePrimaryKey = useCallback(() => { + onChange((prev) => { + return produce(prev, (draft) => { + // Finding the primary key constraint + draft.constraints.forEach((constraint) => { + if (constraint.new?.primaryColumns) { + // Remove the column from the primary key constraint + constraint.new.primaryColumns = + constraint.new.primaryColumns.filter( + (column) => column !== columnName + ); + } + }); + + // Remove empty primary constraint + draft.constraints = draft.constraints.filter((constraint) => { + if (constraint.new?.primaryKey && constraint.new?.primaryColumns) { + return constraint.new.primaryColumns.length > 0; + } + return true; + }); + }); + }); + }, [onChange, columnName]); + + if (!isPrimaryKey) { + return null; + } + return ( - - - - - - - -
-
Primary Key
- - { - onChange({ - constraint: { - primaryKeyConflict: v, - }, - }); - }} - /> - -
-
-
+ ); } diff --git a/src/components/gui/schema-editor/column-provider.tsx b/src/components/gui/schema-editor/column-provider.tsx deleted file mode 100644 index 451b054b..00000000 --- a/src/components/gui/schema-editor/column-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { DatabaseTableColumnChange } from "@/drivers/base-driver"; -import { PropsWithChildren, createContext, useContext } from "react"; - -const ColumnContext = createContext<{ columns: DatabaseTableColumnChange[] }>({ - columns: [], -}); - -export function useColumnList() { - return useContext(ColumnContext); -} - -export function ColumnsProvider({ - children, - value, -}: PropsWithChildren<{ value: DatabaseTableColumnChange[] }>) { - return ( - - {children} - - ); -} diff --git a/src/components/gui/schema-editor/constraint-foreign-key.tsx b/src/components/gui/schema-editor/constraint-foreign-key.tsx new file mode 100644 index 00000000..937afab2 --- /dev/null +++ b/src/components/gui/schema-editor/constraint-foreign-key.tsx @@ -0,0 +1,198 @@ +import { Input } from "@/components/ui/input"; +import { + DatabaseTableConstraintChange, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { Key, Plus } from "@phosphor-icons/react"; +import { produce } from "immer"; +import { ArrowRight, Trash2 } from "lucide-react"; +import { Dispatch, SetStateAction } from "react"; +import TableColumnCombobox from "../table-combobox/TableColumnCombobox"; +import TableCombobox from "../table-combobox/TableCombobox"; +import SchemaEditorColumnCombobox from "./schema-column-combobox"; +import SchemaNameSelect from "./schema-name-select"; + +export default function ConstraintForeignKeyEditor({ + value, + onChange, +}: { + value: DatabaseTableConstraintChange; + onChange: Dispatch>; +}) { + const columnSourceList = + value.new?.foreignKey?.columns ?? value.old?.foreignKey?.columns ?? []; + + const columnDestinationList = + value.new?.foreignKey?.foreignColumns ?? + value.old?.foreignKey?.foreignColumns ?? + []; + + return ( +
+
+ + Foreign Key +
+ + { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new) { + c.new.name = e.target.value; + } + }); + }); + }); + }} + /> + +
Reference Table
+
+ { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new) { + (c.new.foreignKey || {}).foreignSchemaName = e; + } + }); + }); + }); + }} + value={value.new?.foreignKey?.foreignSchemaName} + /> +
+ { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new) { + (c.new.foreignKey || {}).foreignTableName = e; + } + }); + }); + }); + }} + value={value.new?.foreignKey?.foreignTableName} + /> +
+
+ +
+ {columnSourceList.map((column, columnIdx) => { + return ( +
+ {columnSourceList.length > 1 && ( +
+ +
+ )} +
+ { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new && c.new.foreignKey) { + if ( + c.id === value.id && + c.new && + c.new.foreignKey + ) { + if ((c.new.foreignKey.columns || []).length > 0) { + (c.new.foreignKey.columns || [])[columnIdx] = e; + } + } + } + }); + }); + }); + }} + value={column} + /> +
+ + { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new && c.new.foreignKey) { + if ( + (c.new.foreignKey.foreignColumns || []).length > 0 + ) { + (c.new.foreignKey.foreignColumns || [])[columnIdx] = + e; + } else { + c.new.foreignKey.foreignColumns = [e]; + } + } + }); + }); + }); + }} + value={ + (value.new?.foreignKey?.foreignColumns || [])[columnIdx] + ? (value.new?.foreignKey?.foreignColumns || [])[columnIdx] + : undefined + } + schemaName={value.new?.foreignKey?.foreignSchemaName ?? ""} + tableName={value.new?.foreignKey?.foreignTableName ?? ""} + /> +
+ ); + })} + + +
+
+ ); +} diff --git a/src/components/gui/schema-editor/constraint-primary-key.tsx b/src/components/gui/schema-editor/constraint-primary-key.tsx new file mode 100644 index 00000000..69d6fc36 --- /dev/null +++ b/src/components/gui/schema-editor/constraint-primary-key.tsx @@ -0,0 +1,63 @@ +import { Input } from "@/components/ui/input"; +import { + DatabaseTableConstraintChange, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { Key } from "@phosphor-icons/react"; +import { produce } from "immer"; +import { Dispatch, SetStateAction } from "react"; +import ColumnListEditor from "../column-list-editor"; +import { useSchemaEditorContext } from "./schema-editor-prodiver"; + +export default function ConstraintPrimaryKeyEditor({ + value, + onChange, +}: { + value: DatabaseTableConstraintChange; + onChange: Dispatch>; +}) { + const { columns } = useSchemaEditorContext(); + + return ( +
+
+ + Primary Key +
+ + { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new) { + c.new.name = e.target.value; + } + }); + }); + }); + }} + /> + +
+ c.new?.name).filter(Boolean) as string[]} + onChange={(newColumns) => { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints.forEach((c) => { + if (c.id === value.id && c.new) { + c.new.primaryColumns = newColumns; + } + }); + }); + }); + }} + /> +
+
+ ); +} diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index 2cc81146..efc5f235 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -1,213 +1,187 @@ -import { LucideCode, LucideCopy, LucidePlus, LucideSave } from "lucide-react"; -import { Separator } from "../../ui/separator"; -import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; -import { Button, buttonVariants } from "../../ui/button"; -import SchemaEditorColumnList from "./schema-editor-column-list"; +import { InlineTab, InlineTabItem } from "@/components/inline-tab"; +import { + ColumnTypeSelector, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { produce } from "immer"; +import { Dispatch, SetStateAction, useCallback, useState } from "react"; import { Input } from "../../ui/input"; +import SchemaEditorColumnList from "./schema-editor-column-list"; import SchemaEditorConstraintList from "./schema-editor-constraint-list"; -import { ColumnsProvider } from "./column-provider"; -import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; -import CodePreview from "../code-preview"; -import { toast } from "sonner"; -import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; -import { useDatabaseDriver } from "@/context/driver-provider"; +import { SchemaEditorContextProvider } from "./schema-editor-prodiver"; import SchemaNameSelect from "./schema-name-select"; -import { checkSchemaChange } from "@/lib/sql/sql-generate.schema"; interface Props { - onSave: () => void; - onDiscard: () => void; value: DatabaseTableSchemaChange; onChange: Dispatch>; + + /** + * Some database does not support editing existing column such as Sqlite + */ + disabledEditExistingColumn?: boolean; + + /** + * Tell the editor to always use table constraint instead of column constraint + * for primary key and foreign key. + * + * ```sql + * -- Using column constraint style + * CREATE TABLE exampleTable( + * id INTEGER PRIMARY KEY + * ) + * ``` + * To + * ```sql + * -- Using table constraint style + * CREATE TABLE exampleTable( + * id INTEGER, + * PRIMARY KEY(id) + * ) + * ``` + */ + alwayUseTableConstraint?: boolean; + + /** + * Provide column data type suggestion + */ + dataTypeSuggestion: ColumnTypeSelector; + collations?: string[]; } export default function SchemaEditor({ value, onChange, - onSave, - onDiscard, + disabledEditExistingColumn, + dataTypeSuggestion, + collations, }: Readonly) { - const { databaseDriver } = useDatabaseDriver(); + const [selectedTab, setSelectedTab] = useState(0); const isCreateScript = value.name.old === ""; const onAddColumn = useCallback(() => { - const newColumn = - value.columns.length === 0 - ? { - name: "id", - type: databaseDriver.columnTypeSelector.idTypeName ?? "INTEGER", - constraint: { - primaryKey: true, - }, + onChange( + produce(value, (draft) => { + let columnName = value.columns.length === 0 ? "id" : "column"; + + // Dictorary of used column name + const columnNameSet = new Set( + value.columns.map((c) => (c.new?.name ?? c.old?.name)?.toLowerCase()) + ); + + if (columnNameSet.has(columnName)) { + // Finding the next available column name + let columnSuffix = 2; + while (columnNameSet.has(`${columnName}${columnSuffix}`)) { + columnSuffix++; + } + + columnName = `${columnName}${columnSuffix}`; } - : { - name: "column", - type: databaseDriver.columnTypeSelector.textTypeName ?? "TEXT", - constraint: {}, - }; - onChange({ - ...value, - columns: [ - ...value.columns, - { + const newColumn = + value.columns.length === 0 + ? { + name: "id", + type: dataTypeSuggestion.idTypeName ?? "INTEGER", + } + : { + name: columnName, + type: dataTypeSuggestion.textTypeName ?? "TEXT", + constraint: {}, + }; + + draft.columns.push({ key: window.crypto.randomUUID(), old: null, new: newColumn, - }, - ], - }); - }, [value, onChange, databaseDriver]); + }); - const hasChange = checkSchemaChange(value); - - const previewScript = useMemo(() => { - return databaseDriver.createUpdateTableSchema(value).join(";\n"); - }, [value, databaseDriver]); - - const editorOptions = useMemo(() => { - return { - collations: databaseDriver.getCollationList(), - }; - }, [databaseDriver]); + if (value.columns.length === 0) { + draft.constraints.push({ + id: window.crypto.randomUUID(), + old: null, + new: { + primaryKey: true, + primaryColumns: ["id"], + }, + }); + } + }) + ); + }, [value, onChange, dataTypeSuggestion]); return ( -
+ <>
-
- - - -
- -
- - - -
- -
- - - -
- - SQL Preview -
-
- -
SQL Preview
-
- -
-
-
- - {value.createScript && ( - - -
- - Create Script -
-
- - -
- -
-
-
- )} -
-
-
Table Name
{ - onChange({ - ...value, - name: { - ...value.name, - new: e.currentTarget.value, - }, - }); + onChange( + produce(value, (draft) => { + draft.name.new = e.target.value; + }) + ); }} className="w-[200px]" />
-
Schema
{ - onChange({ ...value, schemaName: selectedSchema }); + onChange( + produce(value, (draft) => { + draft.schemaName = selectedSchema; + }) + ); }} />
-
- - - - + + + 0 + ? `Columns (${value.columns.length})` + : "Columns" + } + > + + + 0 + ? `Constraints (${value.constraints.length})` + : "Constraints" + } + > + + + Indexes + +
-
+ ); } diff --git a/src/components/gui/schema-editor/schema-column-combobox.tsx b/src/components/gui/schema-editor/schema-column-combobox.tsx new file mode 100644 index 00000000..c13d90f9 --- /dev/null +++ b/src/components/gui/schema-editor/schema-column-combobox.tsx @@ -0,0 +1,37 @@ +import Combobox from "@/components/combobox"; +import { useMemo } from "react"; +import { useSchemaEditorContext } from "./schema-editor-prodiver"; + +interface SchemaColumnComboboxProps { + value?: string; + onChange: (value: string) => void; + excludedColumns?: string[]; +} + +export default function SchemaEditorColumnCombobox({ + value, + onChange, + excludedColumns, +}: SchemaColumnComboboxProps) { + const { columns } = useSchemaEditorContext(); + + const columnsList = useMemo(() => { + const tempList = columns + .map((c) => c.new?.name) + .filter(Boolean) as string[]; + + if (excludedColumns) { + return tempList.filter((column) => !excludedColumns.includes(column)); + } + + return tempList; + }, [columns, excludedColumns]); + + return ( + ({ value: column, text: column }))} + value={value} + onChange={onChange} + /> + ); +} diff --git a/src/components/gui/schema-editor/schema-editor-column-list.tsx b/src/components/gui/schema-editor/schema-editor-column-list.tsx index d30858f5..ec196acd 100644 --- a/src/components/gui/schema-editor/schema-editor-column-list.tsx +++ b/src/components/gui/schema-editor/schema-editor-column-list.tsx @@ -1,4 +1,27 @@ -import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; +// import { useCommonDialog } from "@/components/common-dialog"; +import { Checkbox } from "@/components/ui/checkbox"; +import { useDatabaseDriver } from "@/context/driver-provider"; +import { useSchema } from "@/context/schema-provider"; +import { + DatabaseTableColumn, + DatabaseTableColumnConstraint, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { checkSchemaColumnChange } from "@/lib/sql/sql-generate.schema"; +import { cn } from "@/lib/utils"; +import { DndContext, DragEndEvent } from "@dnd-kit/core"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { + SortableContext, + arrayMove, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Key } from "@phosphor-icons/react"; +import { produce } from "immer"; +import { LucidePlus, LucideTrash2, PlusIcon } from "lucide-react"; +import { Dispatch, SetStateAction, useCallback, useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -7,7 +30,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu"; -import { LucidePlus, LucideTrash2 } from "lucide-react"; import { Select, SelectContent, @@ -15,34 +37,16 @@ import { SelectTrigger, SelectValue, } from "../../ui/select"; -import { CSS } from "@dnd-kit/utilities"; -import { Checkbox } from "@/components/ui/checkbox"; +import { Toolbar, ToolbarButton, ToolbarSeparator } from "../toolbar"; +import ColumnCheckPopup from "./column-check-popup"; +import ColumnCollation from "./column-collation"; import ColumnDefaultValueInput from "./column-default-value-input"; -import { - DatabaseTableColumn, - DatabaseTableColumnChange, - DatabaseTableColumnConstraint, - DatabaseTableSchemaChange, -} from "@/drivers/base-driver"; -import { cn } from "@/lib/utils"; -import ColumnPrimaryKeyPopup from "./column-pk-popup"; -import ColumnUniquePopup from "./column-unique-popup"; import ColumnForeignKeyPopup from "./column-fk-popup"; import ColumnGeneratingPopup from "./column-generate-popup"; -import ColumnCheckPopup from "./column-check-popup"; -import { Button } from "@/components/ui/button"; -import { DndContext, DragEndEvent } from "@dnd-kit/core"; -import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; -import { useDatabaseDriver } from "@/context/driver-provider"; import ColumnTypeSelector from "./column-type-selector"; -import ColumnCollation from "./column-collation"; -import { checkSchemaColumnChange } from "@/lib/sql/sql-generate.schema"; +import ColumnUniquePopup from "./column-unique-popup"; +import { SchemaEditorForeignKey } from "./schema-editor-foreign-key"; +import { useSchemaEditorContext } from "./schema-editor-prodiver"; export type ColumnChangeEvent = ( newValue: Partial | null @@ -58,40 +62,21 @@ function changeColumnOnIndex( onChange: Dispatch> ) { onChange((prev) => { - if (prev) { - const columns = [...(prev?.columns ?? [])]; - const currentCell = columns[idx] as DatabaseTableColumnChange; - - if (currentCell.new) { - currentCell.new = - value === null - ? null - : { - ...currentCell.new, - ...value, - constraint: value?.constraint - ? { - ...currentCell.new?.constraint, - ...value?.constraint, - } - : currentCell.new?.constraint, - }; - - if (!currentCell.new && !currentCell.old) { - // remove the column - return { - ...prev, - columns: columns.filter((_, colIdx) => colIdx !== idx), - }; - } - - return { - ...prev, - columns, + return produce(prev, (draft) => { + const currentColumn = draft.columns[idx]; + if (currentColumn.new) { + currentColumn.new = { + ...currentColumn.new, + ...value, + constraint: value?.constraint + ? { + ...currentColumn.new.constraint, + ...value.constraint, + } + : currentColumn.new.constraint, }; } - } - return prev; + }); }); } @@ -105,6 +90,7 @@ function ColumnItemType({ disabled?: boolean; }) { const { databaseDriver } = useDatabaseDriver(); + const { suggestion } = useSchemaEditorContext(); if ( databaseDriver.columnTypeSelector.type === "dropdown" && @@ -120,12 +106,16 @@ function ColumnItemType({ onValueChange={onChange} disabled={disabled} > - + - {databaseDriver.columnTypeSelector.dropdownOptions.map((option) => ( - + {suggestion.dropdownOptions?.map((option) => ( + {option.text} ))} @@ -138,26 +128,30 @@ function ColumnItemType({ ); } function ColumnItem({ - value, + schema, + selected, + onSelectChange, idx, schemaName, onChange, - options, disabledEditExistingColumn, }: { - value: DatabaseTableColumnChange; + selected?: boolean; + onSelectChange: (selected: boolean) => void; + schema: DatabaseTableSchemaChange; idx: number; schemaName?: string; onChange: Dispatch>; disabledEditExistingColumn?: boolean; - options: SchemaEditorOptions; }) { + const value = schema.columns[idx]!; + const { setNodeRef, attributes, @@ -173,6 +167,10 @@ function ColumnItem({ transition, }; + const { collations } = useSchemaEditorContext(); + + const supportCollation = collations && collations.length > 0; + const change = useCallback( (newValue: Partial | null) => { changeColumnOnIndex(idx, newValue, onChange); @@ -183,13 +181,28 @@ function ColumnItem({ const column = value.new || value.old; if (!column) return null; - let highlightClassName = ""; + const isPrimaryKey = schema.constraints.some((constraint) => { + return (constraint.new ?? constraint.old)?.primaryColumns?.includes( + column.name + ); + }); + + const isForeignKey = schema.constraints.some((constraint) => { + return (constraint.new ?? constraint.old)?.foreignKey?.columns?.includes( + column.name + ); + }); + + let rowBackgroundColor = "bg-background"; + if (value.new === null) { - highlightClassName = "bg-red-400 dark:bg-red-800"; - } else if (value.old === null) { - highlightClassName = "bg-green-500 dark:bg-green-800"; - } else if (checkSchemaColumnChange(value)) { - highlightClassName = "bg-yellow-400"; + rowBackgroundColor = "bg-red-100 dark:bg-red-400 dark:text-black"; + } else if (schema.name.old && value.old === null) { + rowBackgroundColor = "bg-green-100 dark:bg-green-400 dark:text-black"; + } else if (schema.name.old && checkSchemaColumnChange(value)) { + rowBackgroundColor = "bg-yellow-100 dark:bg-yellow-400 dark:text-black"; + } else if (selected) { + rowBackgroundColor = "bg-gray-100"; } return ( @@ -197,33 +210,52 @@ function ColumnItem({ style={style} {...attributes} ref={setNodeRef} - className={ - value.new === null - ? "bg-red-100 dark:bg-red-400 dark:text-black" - : "bg-background" - } + className={rowBackgroundColor} > -
+
+ {isPrimaryKey ? ( + + ) : ( + "" + )} + {isForeignKey ? ( + + ) : ( + "" + )} + {idx + 1} +
- + + onSelectChange(!selected)} + > +
+ +
+ + change({ name: e.currentTarget.value })} - className="p-2 text-sm outline-none w-[150px] bg-inherit" + className="w-[150px] bg-inherit p-2 font-mono text-sm outline-none" spellCheck={false} /> change({ type: newType })} + onChange={(newType) => { + change({ type: newType }); + }} disabled={disabled} /> @@ -236,7 +268,7 @@ function ColumnItem({ disabled={disabled} /> - +
- {column.constraint?.primaryKey && ( - - )} - {column.constraint?.unique && ( - @@ -359,7 +383,7 @@ function ColumnItem({
- {options.collations.length > 0 && ( + {supportCollation && ( )} - - - ); } export default function SchemaEditorColumnList({ - columns, + value, onChange, - schemaName, onAddColumn, disabledEditExistingColumn, - options, }: Readonly<{ - columns: DatabaseTableColumnChange[]; + value: DatabaseTableSchemaChange; onChange: Dispatch>; - schemaName?: string; onAddColumn: () => void; disabledEditExistingColumn?: boolean; - options: SchemaEditorOptions; }>) { - const headerStyle = "text-xs p-2 text-left bg-secondary border"; + const { currentSchemaName } = useSchema(); + const headerStyle = "text-xs p-2 text-left border-x font-mono"; + + const columns = value.columns; + const schemaName = value.schemaName; + + const { collations } = useSchemaEditorContext(); + + const [selectedColumns, setSelectedColumns] = useState>( + () => new Set() + ); + const [showForeignKey, setShowForeignKey] = useState(false); const handleDragEnd = useCallback( (event: DragEndEvent) => { @@ -426,44 +447,192 @@ export default function SchemaEditorColumnList({ [columns, onChange] ); - const headerCounter = useMemo(() => { - let initialCounter = 7; - if (options.collations.length > 0) { - initialCounter++; - } - - return initialCounter; - }, [options]); + const onRemoveColumns = useCallback(() => { + onChange((prev) => { + return produce(prev, (draft) => { + // If it is a new column, we can just remove it without + // showing the red highlight + draft.columns = draft.columns.filter((col) => { + if (selectedColumns.has(col.key) && !col.old) return false; + return true; + }); + + // If it is an existing column, we need to show the red highlight + draft.columns.forEach((col) => { + if (selectedColumns.has(col.key) && col.old) { + col.new = null; + } + }); + }); + }); + + setSelectedColumns(new Set()); + }, [selectedColumns, onChange, setSelectedColumns]); + + const onSetPrimaryKey = useCallback(() => { + onChange((prev) => { + return produce(prev, (draft) => { + const selectColumnRefList = draft.columns.filter((c) => + selectedColumns.has(c.key) + ); + + // Finding existing primary key constraint + const existingPrimaryKey = draft.constraints.find( + (c) => c.new?.primaryKey + )?.new; + + if (existingPrimaryKey) { + existingPrimaryKey.primaryColumns = + existingPrimaryKey.primaryColumns ?? []; + + for (const columnRef of selectColumnRefList) { + if ( + !existingPrimaryKey.primaryColumns.includes( + columnRef.new?.name ?? "" + ) + ) { + existingPrimaryKey.primaryColumns.push(columnRef.new?.name ?? ""); + } + } + } else { + draft.constraints.push({ + id: window.crypto.randomUUID(), + old: null, + new: { + primaryKey: true, + primaryColumns: selectColumnRefList.map( + (c) => c.new?.name ?? c.old?.name ?? "" + ), + }, + }); + } + }); + }); + + setSelectedColumns(new Set()); + }, [selectedColumns, onChange, setSelectedColumns]); + + const onSetForeignKey = useCallback(() => { + onChange((prev) => { + return produce(prev, (draft) => { + const selectColumnRefList = draft.columns.filter((c) => + selectedColumns.has(c.key) + ); + + const existingForeignKey = draft.constraints + .filter((c) => c.new?.foreignKey) + .map((c) => c.new?.foreignKey?.columns) + .flat(); + + if (existingForeignKey.length > 0) { + for (const columnRef of selectColumnRefList) { + if (!existingForeignKey.includes(columnRef.new?.name ?? "")) { + for (const columnRef of selectColumnRefList) { + draft.constraints.push({ + id: columnRef.key, + old: null, + new: { + name: `fk_${columnRef.new?.name}`, + foreignKey: { + columns: [columnRef.new?.name ?? ""], + onDelete: "CASCADE", + onUpdate: "CASCADE", + }, + }, + }); + } + } + } + } else { + for (const columnRef of selectColumnRefList) { + draft.constraints.push({ + id: columnRef.key, + old: null, + new: { + name: `fk_${columnRef.new?.name}`, + foreignKey: { + columns: [columnRef.new?.name ?? ""], + foreignSchemaName: currentSchemaName, + }, + }, + }); + } + } + }); + }); + setShowForeignKey(true); + }, [selectedColumns, onChange, currentSchemaName]); return ( -
- {options.collations.length > 0 && ( +
+ {collations.length > 0 && ( - {options.collations.map((collation) => ( + {collations.map((collation) => ( )} + {showForeignKey && ( + setShowForeignKey(false)} + /> + )} + +
+ + } + onClick={onAddColumn} + /> + } + destructive + disabled={selectedColumns.size === 0} + onClick={onRemoveColumns} + /> + + } + onClick={onSetPrimaryKey} + disabled={selectedColumns.size === 0} + /> + + +
+ - +
- - + + + - {options.collations.length > 0 && ( + {collations.length > 0 && ( )} - + {/* */} @@ -473,26 +642,27 @@ export default function SchemaEditorColumnList({ > {columns.map((col, idx) => ( { + setSelectedColumns((prev) => { + if (selected) { + prev.add(col.key); + } else { + prev.delete(col.key); + } + return new Set(prev); + }); + }} idx={idx} - value={col} + schema={value} key={col.key} onChange={onChange} schemaName={schemaName} disabledEditExistingColumn={disabledEditExistingColumn} - options={options} /> ))} - - - - -
Name + # + Name Type Default Null ConstraintCollation
- -
diff --git a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx index 09958970..71e14330 100644 --- a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx +++ b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx @@ -1,36 +1,21 @@ +import { Checkbox } from "@/components/ui/checkbox"; import { DatabaseTableColumnConstraint, DatabaseTableConstraintChange, DatabaseTableSchemaChange, } from "@/drivers/base-driver"; import { cn } from "@/lib/utils"; -import { - LucideArrowUpRight, - LucideCheck, - LucideFingerprint, - LucideKeySquare, - LucideShieldPlus, - LucideTrash2, -} from "lucide-react"; -import TableCombobox from "../table-combobox/TableCombobox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../../ui/dropdown-menu"; -import { Button } from "../../ui/button"; -import { - Dispatch, - PropsWithChildren, - SetStateAction, - useCallback, - useMemo, -} from "react"; -import ColumnListEditor from "../column-list-editor"; -import { useColumnList } from "./column-provider"; -import { useSchema } from "@/context/schema-provider"; - +import { Plus } from "@phosphor-icons/react"; +import { produce } from "immer"; +import { LucideTrash2 } from "lucide-react"; +import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { DropdownMenuItem } from "../../ui/dropdown-menu"; +import { Toolbar, ToolbarButton, ToolbarDropdown } from "../toolbar"; +import ConstraintForeignKeyEditor from "./constraint-foreign-key"; +import ConstraintPrimaryKeyEditor from "./constraint-primary-key"; + +// ---- not remove this one because maybe can be used. +/* type ConstraintChangeHandler = ( constraint: DatabaseTableColumnConstraint ) => void; @@ -77,7 +62,7 @@ function ColumnForeignKey({ disabled?: boolean; schemaName: string; }>) { - const { columns } = useColumnList(); + const { columns } = useSchemaEditorContext(); const { schema } = useSchema(); const columnMemo = useMemo(() => { @@ -175,7 +160,7 @@ function ColumnPrimaryKey({ onChange: ConstraintChangeHandler; disabled?: boolean; }>) { - const { columns } = useColumnList(); + const { columns } = useSchemaEditorContext(); const columnMemo = useMemo(() => { return [...new Set(columns.map((c) => c.new?.name ?? c.old?.name ?? ""))]; @@ -217,7 +202,7 @@ function ColumnUnique({ onChange: ConstraintChangeHandler; disabled?: boolean; }>) { - const { columns } = useColumnList(); + const { columns } = useSchemaEditorContext(); const columnMemo = useMemo(() => { return [...new Set(columns.map((c) => c.new?.name ?? c.old?.name ?? ""))]; @@ -249,7 +234,7 @@ function ColumnUnique({ ); } - + function RemovableConstraintItem({ children, idx, @@ -288,6 +273,7 @@ function RemovableConstraintItem({ ); } + function ColumnItemBody({ onChange, schemaName, @@ -362,9 +348,9 @@ function ColumnItemBody({ } return ; -} +}*/ -function ColumnItem({ +/*function ColumnItem({ constraint, onChange, idx, @@ -388,20 +374,39 @@ function ColumnItem({ /> ); +}*/ + +function ConstraintListItem({ + value, + onChange, +}: { + value: DatabaseTableConstraintChange; + onChange: Dispatch>; +}) { + if (value.new?.primaryKey) { + return ; + } else if (value.new?.foreignKey) { + return ; + } + + return
Not implemented
; } export default function SchemaEditorConstraintList({ constraints, onChange, - schemaName, - disabled, + // schemaName, + // disabled, }: Readonly<{ constraints: DatabaseTableConstraintChange[]; onChange: Dispatch>; schemaName?: string; disabled?: boolean; }>) { - const headerClassName = "text-xs p-2 text-left bg-secondary border"; + const headerClassName = "text-xs p-2 text-left border-l"; + const [selectedColumns, setSelectedColumns] = useState>( + () => new Set() + ); const newConstraint = useCallback( (con: DatabaseTableColumnConstraint) => { @@ -420,82 +425,123 @@ export default function SchemaEditorConstraintList({ [onChange] ); + const hasPrimaryKey = constraints.some((c) => c.new?.primaryKey); + + const onRemoveConstraint = useCallback(() => { + onChange((prev) => { + return produce(prev, (draft) => { + draft.constraints = draft.constraints.filter((col) => { + if (selectedColumns.has(col.id) && !col.old) return false; + return true; + }); + + draft.constraints.forEach((col) => { + if (selectedColumns.has(col.id) && col.old) { + col.new = null; + } + }); + }); + }); + setSelectedColumns(new Set()); + }, [selectedColumns, onChange, setSelectedColumns]); + + const onSelectChange = useCallback((selected: boolean, id: string) => { + setSelectedColumns((prev) => { + if (selected) { + prev.add(id); + } else { + prev.delete(id); + } + return new Set(prev); + }); + }, []); + return ( -
- +
+
+ + }> + { + newConstraint({ primaryKey: true }); + }} + > + Primary Key + + { + newConstraint({ unique: true }); + }} + > + Unique + + { + newConstraint({ checkExpression: "" }); + }} + > + Check Constraint + + { + newConstraint({ + foreignKey: { + columns: [], + }, + }); + }} + > + Foreign Key + + + } + disabled={selectedColumns.size === 0} + onClick={onRemoveConstraint} + destructive + /> + +
+
- - - - + + + {constraints.map((constraint, idx) => { + const selected = selectedColumns.has(constraint.id); return ( - + + + + + ); })} - {!disabled && ( - - - - )}
Constraints + # + Constraint
+ {idx + 1} + onSelectChange(!selected, constraint.id)} + > + + + +
- - - - - - { - newConstraint({ primaryKey: true }); - }} - > - Primary Key - - { - newConstraint({ unique: true }); - }} - > - Unique - - { - newConstraint({ checkExpression: "" }); - }} - > - Check Constraint - - { - newConstraint({ - foreignKey: { - columns: [], - }, - }); - }} - > - Foreign Key - - - -
diff --git a/src/components/gui/schema-editor/schema-editor-foreign-key.tsx b/src/components/gui/schema-editor/schema-editor-foreign-key.tsx new file mode 100644 index 00000000..67b40893 --- /dev/null +++ b/src/components/gui/schema-editor/schema-editor-foreign-key.tsx @@ -0,0 +1,55 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DatabaseTableConstraintChange, + DatabaseTableSchemaChange, +} from "@/drivers/base-driver"; +import { Dispatch, SetStateAction } from "react"; +import ConstraintForeignKeyEditor from "./constraint-foreign-key"; + +interface Props { + selectedColumns: Set; + constraints: DatabaseTableConstraintChange[]; + onChange: Dispatch>; + onClose: () => void; +} + +export function SchemaEditorForeignKey(props: Props) { + console.log(props); + return ( + + + Foreign Key + +
+ + + {props.constraints + .filter((x) => props.selectedColumns.has(x.id)) + .map((x, idx) => { + return ( + + + + + ); + })} + +
+ {idx + 1} + + +
+
+
+
+
+ ); +} diff --git a/src/components/gui/schema-editor/schema-editor-prodiver.tsx b/src/components/gui/schema-editor/schema-editor-prodiver.tsx new file mode 100644 index 00000000..e0c2a54f --- /dev/null +++ b/src/components/gui/schema-editor/schema-editor-prodiver.tsx @@ -0,0 +1,46 @@ +import { + ColumnTypeSelector, + DatabaseTableColumnChange, +} from "@/drivers/base-driver"; +import { PropsWithChildren, createContext, useContext, useMemo } from "react"; + +const ColumnContext = createContext<{ + columns: DatabaseTableColumnChange[]; + suggestion: ColumnTypeSelector; + collations: string[]; +}>({ + columns: [], + collations: [], + suggestion: { + type: "dropdown", + dropdownOptions: [], + }, +}); + +export function useSchemaEditorContext() { + return useContext(ColumnContext); +} + +export function SchemaEditorContextProvider({ + children, + value, + suggestion, + collations, + alwayUseTableConstraint, +}: PropsWithChildren<{ + value: DatabaseTableColumnChange[]; + suggestion: ColumnTypeSelector; + collations: string[]; + alwayUseTableConstraint?: boolean; +}>) { + const providerValue = useMemo( + () => ({ columns: value, suggestion, collations, alwayUseTableConstraint }), + [value, suggestion, collations, alwayUseTableConstraint] + ); + + return ( + + {children} + + ); +} diff --git a/src/components/gui/tabs/mass-drop-table.tsx b/src/components/gui/tabs/mass-drop-table.tsx index dc26139e..56da087f 100644 --- a/src/components/gui/tabs/mass-drop-table.tsx +++ b/src/components/gui/tabs/mass-drop-table.tsx @@ -1,9 +1,5 @@ import { SelectableTable } from "@/components/selectable-table"; -import { useSchema } from "@/context/schema-provider"; -import { DatabaseSchemaItem } from "@/drivers/base-driver"; -import { Check, Spinner, Table, Trash, XCircle } from "@phosphor-icons/react"; -import { ReactElement, useCallback, useEffect, useState } from "react"; -import { Toolbar, ToolbarButton } from "../toolbar"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -11,8 +7,12 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; import { useDatabaseDriver } from "@/context/driver-provider"; +import { useSchema } from "@/context/schema-provider"; +import { DatabaseSchemaItem } from "@/drivers/base-driver"; +import { Check, Spinner, Table, Trash, XCircle } from "@phosphor-icons/react"; +import { ReactElement, useCallback, useEffect, useState } from "react"; +import { Toolbar, ToolbarButton } from "../toolbar"; function ConfirmDialog({ selectedItems, @@ -37,14 +37,14 @@ function ConfirmDialog({
You are about to drop the following tables. -
+
{selectedItems.map((t) => ( -
+
{t.name}
))}
-

+

ln(x) + ex-1 - cos(x) = 0

Solve this equaltion or type confirm

@@ -168,27 +168,27 @@ export default function MassDropTableTab() { if (status === "Failed") statusIcon = ( - + ); else if (status.includes("...")) statusIcon = ( - + ); else if (status === "Dropped" || status === "Emptied") statusIcon = ( - + ); return ( <> - + {t.name} - - @@ -217,7 +217,7 @@ export default function MassDropTableTab() { )}
-

+

Drop & Empty Multiple Tables

@@ -225,7 +225,7 @@ export default function MassDropTableTab() { } + icon={} text="Drop Selected Table" onClick={dropSelectedTableClicked} destructive @@ -240,7 +240,7 @@ export default function MassDropTableTab() {
-
+
{ @@ -53,35 +51,6 @@ export default function SchemaEditorTab({ } }, [fetchTable, schemaName, tableName]); - const previewScript = useMemo(() => { - return databaseDriver.createUpdateTableSchema(schema); - }, [schema, databaseDriver]); - - const onSaveToggle = useCallback( - () => setIsSaving((prev) => !prev), - [setIsSaving] - ); - - const onDiscard = useCallback(() => { - setSchema((prev) => { - return { - name: { ...prev.name, new: prev.name.old }, - columns: prev.columns - .map((col) => ({ - key: col.key, - old: col.old, - new: cloneDeep(col.old), - })) - .filter((col) => col.old), - constraints: prev.constraints.map((con) => ({ - id: window.crypto.randomUUID(), - old: con.old, - new: cloneDeep(con.old), - })), - }; - }); - }, [setSchema]); - if (loading) { return (
@@ -90,22 +59,23 @@ export default function SchemaEditorTab({ ); } return ( - <> - {isSaving && ( - +
+ - )} +
- +
); } diff --git a/src/components/gui/tabs/schema-editor-tab/toolbar.tsx b/src/components/gui/tabs/schema-editor-tab/toolbar.tsx new file mode 100644 index 00000000..4563369b --- /dev/null +++ b/src/components/gui/tabs/schema-editor-tab/toolbar.tsx @@ -0,0 +1,133 @@ +import { useCommonDialog } from "@/components/common-dialog"; +import { useDatabaseDriver } from "@/context/driver-provider"; +import { useSchema } from "@/context/schema-provider"; +import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import { checkSchemaChange } from "@/lib/sql/sql-generate.schema"; +import { cloneDeep } from "lodash"; +import { CodeIcon, LucideTableProperties, SaveIcon } from "lucide-react"; +import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; +import SchemaEditorTab from "."; +import { + Toolbar, + ToolbarButton, + ToolbarCodePreview, + ToolbarSeparator, +} from "../../toolbar"; +import { useTabsContext } from "../../windows-tab"; + +export default function SchemaEditorToolbar({ + value, + onChange, + fetchTable, +}: { + value: DatabaseTableSchemaChange; + onChange: Dispatch>; + fetchTable: (schemeName: string, tableName: string) => Promise; +}) { + const { refresh: refreshSchema } = useSchema(); + const { replaceCurrentTab } = useTabsContext(); + const { databaseDriver } = useDatabaseDriver(); + const { showDialog } = useCommonDialog(); + + const previewScript = useMemo(() => { + return databaseDriver.createUpdateTableSchema(value); + }, [value, databaseDriver]); + + const hasChange = checkSchemaChange(value); + + const onDiscard = useCallback(() => { + onChange((prev) => { + return { + name: { ...prev.name, new: prev.name.old }, + columns: prev.columns + .map((col) => ({ + key: col.key, + old: col.old, + new: cloneDeep(col.old), + })) + .filter((col) => col.old), + constraints: prev.constraints.map((con) => ({ + id: window.crypto.randomUUID(), + old: con.old, + new: cloneDeep(con.old), + })), + }; + }); + }, [onChange]); + + const onSaveClicked = useCallback(() => { + showDialog({ + title: "Preview", + content: "Are you sure you want to run this change?", + previewCode: previewScript.join("\n"), + actions: [ + { + text: "Continue", + onClick: async () => { + await databaseDriver.transaction(previewScript); + + if (value.name.new !== value.name.old) { + refreshSchema(); + replaceCurrentTab({ + component: ( + + ), + key: "_schema_" + value.name.new, + identifier: "_schema_" + value.name.new, + title: "Edit " + value.name.new, + icon: LucideTableProperties, + }); + } else if (value.name.old && value.schemaName) { + fetchTable( + value.schemaName, + value.name?.new || value.name?.old || "" + ); + } + }, + }, + ], + }); + }, [ + value, + previewScript, + showDialog, + refreshSchema, + databaseDriver, + replaceCurrentTab, + fetchTable, + ]); + + return ( + + } + onClick={onSaveClicked} + disabled={!hasChange} + /> + + + + + {value.createScript && ( + + )} + + ); +} diff --git a/src/components/gui/tabs/table-data-tab.tsx b/src/components/gui/tabs/table-data-tab.tsx index 71299770..dd9ebabf 100644 --- a/src/components/gui/tabs/table-data-tab.tsx +++ b/src/components/gui/tabs/table-data-tab.tsx @@ -1,22 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; import ResultTable from "@/components/gui/query-result-table"; -import { Button } from "@/components/ui/button"; -import { - LucideArrowLeft, - LucideArrowRight, - LucideDelete, - LucideFilter, - LucidePlus, - LucideRefreshCcw, - LucideSaveAll, -} from "lucide-react"; -import { Separator } from "@/components/ui/separator"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { commitChange } from "@/lib/sql/sql-execute-helper"; import { AlertDialog, AlertDialogAction, @@ -24,24 +6,42 @@ import { AlertDialogDescription, AlertDialogFooter, } from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useAutoComplete } from "@/context/auto-complete-provider"; +import { useDatabaseDriver } from "@/context/driver-provider"; +import { useSchema } from "@/context/schema-provider"; import { ColumnSortOption, DatabaseResultStat, DatabaseTableSchema, } from "@/drivers/base-driver"; -import { useAutoComplete } from "@/context/auto-complete-provider"; +import { KEY_BINDING } from "@/lib/key-matcher"; +import { commitChange } from "@/lib/sql/sql-execute-helper"; +import { AlertDialogTitle } from "@radix-ui/react-alert-dialog"; +import { + LucideArrowLeft, + LucideArrowRight, + LucideDelete, + LucideFilter, + LucidePlus, + LucideRefreshCcw, + LucideSaveAll, +} from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import AggregateResultButton from "../aggregate-result/aggregate-result-button"; +import ExportResultButton from "../export/export-result-button"; import OpacityLoading from "../loading-opacity"; -import OptimizeTableState from "../table-optimized/OptimizeTableState"; -import { useDatabaseDriver } from "@/context/driver-provider"; import ResultStats from "../result-stat"; +import OptimizeTableState from "../table-optimized/OptimizeTableState"; import useTableResultColumnFilter from "../table-result/filter-column"; -import { AlertDialogTitle } from "@radix-ui/react-alert-dialog"; -import { useCurrentTab } from "../windows-tab"; -import { KEY_BINDING } from "@/lib/key-matcher"; import { Toolbar, ToolbarButton } from "../toolbar"; -import AggregateResultButton from "../aggregate-result/aggregate-result-button"; -import ExportResultButton from "../export/export-result-button"; -import { useSchema } from "@/context/schema-provider"; +import { useCurrentTab } from "../windows-tab"; interface TableDataContentProps { tableName: string; diff --git a/src/components/gui/toolbar.tsx b/src/components/gui/toolbar.tsx index af2c71d4..81bd2a3c 100644 --- a/src/components/gui/toolbar.tsx +++ b/src/components/gui/toolbar.tsx @@ -1,9 +1,19 @@ -import { PropsWithChildren, ReactElement } from "react"; -import { buttonVariants } from "../ui/button"; -import { LucideLoader } from "lucide-react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import { Separator } from "../ui/separator"; import { cn } from "@/lib/utils"; +import { Icon } from "@phosphor-icons/react"; +import { LucideCopy, LucideLoader } from "lucide-react"; +import { cloneElement, forwardRef, PropsWithChildren, ReactElement, useState } from "react"; +import { toast } from "sonner"; +import { Button, buttonVariants } from "../ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { Separator } from "../ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import CodePreview from "./code-preview"; + export function Toolbar({ children }: PropsWithChildren) { return
{children}
; @@ -17,17 +27,8 @@ export function ToolbarSeparator() { ); } -export function ToolbarButton({ - disabled, - loading, - icon, - onClick, - badge, - text, - tooltip, - destructive, -}: { - icon?: ReactElement; +interface ToolbarButtonProps { + icon?: React.ReactElement; disabled?: boolean; loading?: boolean; badge?: string; @@ -35,40 +36,131 @@ export function ToolbarButton({ onClick?: () => void; tooltip?: ReactElement | string; destructive?: boolean; -}) { - const buttonContent = ( +} + +export const ToolbarButton = forwardRef( + function ToolbarButtonInternal(props: ToolbarButtonProps, ref) { + const { + disabled, + loading, + icon: Icon, + onClick, + badge, + text, + tooltip, + destructive, + } = props; + + const buttonContent = ( + + ); + + if (tooltip) { + return ( + + {buttonContent} + {tooltip} + + ); + } + + return buttonContent; + } +); + +interface ToolbarCodePreviewProps { + code: string; + icon?: Icon; + text: string; +} + +export function ToolbarCodePreview({ + text, + icon: Icon, + code, +}: ToolbarCodePreviewProps) { + const activator = ( ); - if (tooltip) { - return ( - - {buttonContent} - {tooltip} - - ); - } + if (!code) return activator; - return buttonContent; + return ( + + {activator} + + + +
+ +
+
+
+ ); +} + +export function ToolbarDropdown(props: PropsWithChildren) { + const [open, setOpen] = useState(false); + const { children, ...buttonProps } = props; + + return ( + + + { + setOpen(true); + }} + /> + + + {children} + + + ); } diff --git a/src/components/inline-tab.tsx b/src/components/inline-tab.tsx new file mode 100644 index 00000000..2fd1f6d7 --- /dev/null +++ b/src/components/inline-tab.tsx @@ -0,0 +1,55 @@ +import { cn } from "@/lib/utils"; +import React, { PropsWithChildren, ReactElement } from "react"; + +interface InlineTabItemProps { + title: string; +} + +interface InlineTabProps { + children: ReactElement>[]; + selected?: number; + onChange?: (selected: number) => void; +} + +export function InlineTab({ children, selected, onChange }: InlineTabProps) { + const childrenArray = React.Children.toArray(children) as ReactElement< + PropsWithChildren + >[]; + + // Loop through children and get its props + return ( +
+
    +
  • + {childrenArray.map((child, idx) => { + return ( +
  • { + if (selected !== idx && onChange) { + onChange(idx); + } + }} + className={cn( + "px-3 py-2 cursor-pointer", + selected === idx + ? "font-bold border-x border-t rounded-t" + : "border-b" + )} + key={idx} + > + {child.props.title} +
  • + ); + })} +
  • +
+
{childrenArray[selected ?? 0]}
+
+ ); +} + +export function InlineTabItem({ + children, +}: PropsWithChildren) { + return
{children}
; +} diff --git a/src/drivers/mysql/generate-schema.ts b/src/drivers/mysql/generate-schema.ts index 14c61627..beb1eea0 100644 --- a/src/drivers/mysql/generate-schema.ts +++ b/src/drivers/mysql/generate-schema.ts @@ -7,7 +7,7 @@ import { DatabaseTriggerSchema, } from "../base-driver"; -import { omit, isEqual } from "lodash"; +import { isEqual, omit } from "lodash"; function wrapParen(str: string) { if (str.toUpperCase() === "NULL") return str; @@ -102,7 +102,7 @@ function generateCreateColumn( [ "REFERENCES", driver.escapeId(foreignTableName) + - `(${driver.escapeId(foreignColumnName)})`, + `(${driver.escapeId(foreignColumnName)})`, ].join(" ") ); } @@ -122,6 +122,7 @@ function generateConstraintScript( return `CHECK (${con.checkExpression})`; } else if (con.foreignKey) { return ( + `CONSTRAINT ${driver.escapeId(con.name ?? `fk_${driver.escapeId}`)} ` + `FOREIGN KEY (${con.foreignKey.columns?.map(driver.escapeId).join(", ")}) ` + `REFERENCES ${driver.escapeId(con.foreignKey.foreignTableName ?? "")} ` + `(${con.foreignKey.foreignColumns?.map(driver.escapeId).join(", ")})` diff --git a/src/drivers/sqlite/sqlite-generate-schema.ts b/src/drivers/sqlite/sqlite-generate-schema.ts index f0a30419..5267f955 100644 --- a/src/drivers/sqlite/sqlite-generate-schema.ts +++ b/src/drivers/sqlite/sqlite-generate-schema.ts @@ -1,10 +1,10 @@ -import { escapeIdentity, escapeSqlValue } from "@/drivers/sqlite/sql-helper"; import { DatabaseTableColumn, DatabaseTableColumnConstraint, DatabaseTableSchemaChange, } from "@/drivers/base-driver"; -import { omit, isEqual } from "lodash"; +import { escapeIdentity, escapeSqlValue } from "@/drivers/sqlite/sql-helper"; +import { isEqual, omit } from "lodash"; function wrapParen(str: string) { if (str.length >= 2 && str.startsWith("(") && str.endsWith(")")) return str; @@ -108,6 +108,7 @@ function generateConstraintScript(con: DatabaseTableColumnConstraint) { return `CHECK (${con.checkExpression})`; } else if (con.foreignKey) { return ( + `CONSTRAINT ${escapeIdentity(con.name ?? `fk_`)} ` + `FOREIGN KEY (${con.foreignKey.columns?.map(escapeIdentity).join(", ")}) ` + `REFERENCES ${escapeIdentity(con.foreignKey.foreignTableName ?? "")} ` + `(${con.foreignKey.foreignColumns?.map(escapeIdentity).join(", ")})`
+ {t.type} + {statusIcon} {status}