mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Improved Performence , Re-Order Diagram whene import , Fix Database History Bug (stuck in undo or redo , foriegn keys constraint error)
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/react": "18.3.3",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { cn } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
@@ -6,12 +7,12 @@ export interface CardinalityMarkerProps {
|
||||
selected?: boolean,
|
||||
direction?: "start" | "end",
|
||||
cardinality: "one" | "many",
|
||||
type?: "symbole" | "numeric"
|
||||
style?: CardinalityStyle
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", type = "symbole" }) => {
|
||||
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", style = CardinalityStyle.SYMBOLIC }) => {
|
||||
|
||||
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
|
||||
const renderMarker = () => {
|
||||
@@ -37,7 +38,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
}
|
||||
|
||||
|
||||
if (type == "symbole")
|
||||
if (style == CardinalityStyle.SYMBOLIC)
|
||||
return (
|
||||
<>
|
||||
<marker
|
||||
@@ -63,22 +64,22 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
</marker>
|
||||
</>
|
||||
)
|
||||
else if (type == "numeric") {
|
||||
else if (style == CardinalityStyle.NUMERIC) {
|
||||
return (
|
||||
<marker
|
||||
id={id}
|
||||
viewBox="0 0 24 24"
|
||||
markerWidth="24"
|
||||
markerHeight="24"
|
||||
refX={direction == "start" ? "4" : "20"}
|
||||
refX={direction == "start" ? "2" : "22"}
|
||||
refY="12"
|
||||
orient="auto"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
stroke-width="1"
|
||||
r="8"
|
||||
strokeWidth="1"
|
||||
className={
|
||||
cn("dark:fill-default fill-default",
|
||||
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
|
||||
@@ -88,11 +89,11 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
<text
|
||||
x="12"
|
||||
y="13"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-size="7"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize="8"
|
||||
className={
|
||||
cn("fill-font/90 dark:fill-font/90" ,
|
||||
cn("fill-font/90 dark:fill-font/90 font-semibold" ,
|
||||
selected ? "fill-primary dark:fill-primary" : "" ,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
|
||||
value={database.dialect}
|
||||
className="database-checkbox"
|
||||
classNames={{
|
||||
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative",
|
||||
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default" ,
|
||||
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative data-[selected=true]:border-1 data-[selected=true]:border-primary data-[selected=true]:bg-primary/5",
|
||||
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default " ,
|
||||
label : "flex items-center justify-center w-full h-full p-2 "
|
||||
}}
|
||||
|
||||
@@ -26,7 +26,7 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
|
||||
<div className="min-w-full h-full flex items-center justify-center ">
|
||||
<Image
|
||||
src={database.logo}
|
||||
className="w-full"
|
||||
className="w-full rounded-none"
|
||||
/>
|
||||
</div>
|
||||
</Checkbox>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
|
||||
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
interface OptionCheckboxProps {
|
||||
value: string;
|
||||
@@ -13,15 +15,15 @@ interface OptionCheckboxProps {
|
||||
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
|
||||
const { value, icon, logo, label, isSelected } = props;
|
||||
|
||||
const variant = isSelected ? {
|
||||
variant: "flat",
|
||||
color: "primary"
|
||||
let variant : any = isSelected ? {
|
||||
variant: "solid",
|
||||
color: "default"
|
||||
} : {
|
||||
variant: "bordered",
|
||||
variant: "borderd",
|
||||
color: "default"
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
aria-label={value}
|
||||
value={value}
|
||||
@@ -32,16 +34,15 @@ const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
|
||||
label: "flex items-center justify-center w-full h-full "
|
||||
}}
|
||||
>
|
||||
|
||||
<Chip radius="sm" {...variant as any} className={cn("option-span px-3 h-8 border-1 transition-all duration-300 border-divider ",
|
||||
!isSelected ? "dark:bg-default" : undefined
|
||||
<Chip radius="sm" {...variant as any} className={cn("dark:bg-background px-3 h-9 border-1 transition-all duration-300 border-divider text-font/90 ",
|
||||
!isSelected ? "dark:bg-default" : undefined
|
||||
)}
|
||||
avatar={logo ? <img src={logo} /> : undefined}
|
||||
avatar={logo ? <img src={logo} height={12} /> : undefined}
|
||||
startContent={
|
||||
icon
|
||||
}
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
<span className="text-xs font-medium ">
|
||||
{label}
|
||||
</span>
|
||||
</Chip>
|
||||
@@ -50,4 +51,4 @@ const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
|
||||
}
|
||||
|
||||
|
||||
export default OptionCheckbox;
|
||||
export default React.memo(OptionCheckbox);
|
||||
@@ -40,7 +40,7 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="bordered"
|
||||
className="text-font/90 border-divider"
|
||||
className="text-font/90 border-1 border-divider"
|
||||
onPressEnd={copyToClipboard}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button } from "@heroui/react";
|
||||
import { useMemo } from "react";
|
||||
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, Popover, PopoverTrigger, PopoverContent, cn } from "@heroui/react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import SubmenuDropdown from "./submenu-dropdown";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
|
||||
|
||||
@@ -12,78 +13,78 @@ import SubmenuDropdown from "./submenu-dropdown";
|
||||
|
||||
|
||||
export interface MenuDropdownProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
children?: MenuDropdownProps[];
|
||||
|
||||
theme?: "default" | "danger",
|
||||
isDisabled?: boolean,
|
||||
divide?: boolean,
|
||||
shortcut?: string,
|
||||
isOpen?: boolean ,
|
||||
clickHandler? : () => void
|
||||
isOpen?: boolean,
|
||||
selected?: boolean;
|
||||
clickHandler?: () => void
|
||||
}
|
||||
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler }) => {
|
||||
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler, selected = false }) => {
|
||||
|
||||
const disabledChilds: string[] = useMemo(() => {
|
||||
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled && child.title).map((child: MenuDropdownProps) => child.title as string) : []
|
||||
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled).map((child: MenuDropdownProps) => child.id as string) : []
|
||||
}, [children]);
|
||||
|
||||
return <Dropdown radius="sm" shadow="sm" showArrow >
|
||||
return <Dropdown radius="sm" shadow="sm" showArrow>
|
||||
{
|
||||
title &&
|
||||
<DropdownTrigger onPressEnd={clickHandler}>
|
||||
<Button size="sm" variant="light" className="min-w-[42px]" >
|
||||
<DropdownTrigger onPressEnd={clickHandler}>
|
||||
<Button size="sm" variant="light" className="min-w-[42px]" aria-label={title} >
|
||||
<span className=" text-left flex justify-between text-small font-semibold text-font/90">
|
||||
{title}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
}
|
||||
<HeroDropdownMenu disabledKeys={disabledChilds} >
|
||||
|
||||
<HeroDropdownMenu
|
||||
disabledKeys={disabledChilds}
|
||||
>
|
||||
{
|
||||
children ? children?.map((child: MenuDropdownProps) => (
|
||||
|
||||
<DropdownItem
|
||||
key={child.title as string}
|
||||
|
||||
key={child.id as string}
|
||||
color={child.theme}
|
||||
shortcut={child.shortcut}
|
||||
classNames={{
|
||||
shortcut : "dark:border-font/10"
|
||||
shortcut: "dark:border-font/10",
|
||||
base: child.children ? "p-0" : "p-2"
|
||||
}}
|
||||
className="text-font/90"
|
||||
onPressEnd={ child.clickHandler }
|
||||
>
|
||||
textValue={child.title}
|
||||
endContent={
|
||||
child.selected ? <Check className="text-icon size-4" /> : undefined
|
||||
}
|
||||
className={child.theme == "danger" ? "text-danger" : "text-font/90"}
|
||||
onPressEnd={child.clickHandler}
|
||||
>
|
||||
{
|
||||
|
||||
!child.children ?
|
||||
<div >
|
||||
{child.title}
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<div >
|
||||
<SubmenuDropdown {...child} />
|
||||
|
||||
<SubmenuDropdown {...child} />
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider"></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
</DropdownItem>
|
||||
|
||||
)) : []
|
||||
}
|
||||
|
||||
</HeroDropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,34 +5,44 @@ import React, { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
|
||||
|
||||
const Menu: React.FC<any > = ({ }) => {
|
||||
const Menu: React.FC = ({ }) => {
|
||||
|
||||
const { setTheme } = useTheme()
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { open } = useModal();
|
||||
|
||||
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
|
||||
const { deleteMultiTables } = useDatabaseOperations();
|
||||
const { database } = useDatabase();
|
||||
const { setShowController, showController, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
|
||||
|
||||
const menu: MenuDropdownProps[] = useMemo(() => [
|
||||
{
|
||||
id: "menu.file",
|
||||
title: t("menu.file"),
|
||||
children: [
|
||||
{
|
||||
id: "menu.new",
|
||||
title: t("menu.new"),
|
||||
clickHandler: () => {
|
||||
open(Modals.CREATE_DATABASE)
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
id: "menu.open",
|
||||
title: t("menu.open"), shortcut: "Ctnl + O", clickHandler: () => {
|
||||
open(Modals.OPEN_DATABASE)
|
||||
}
|
||||
},
|
||||
{ title: t("menu.save"), shortcut: "Ctnl + S", divide: true },
|
||||
{
|
||||
id: "menu.import",
|
||||
title: t("menu.import"),
|
||||
divide: true,
|
||||
clickHandler: () => {
|
||||
@@ -40,15 +50,15 @@ const Menu: React.FC<any > = ({ }) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "menu.export_sql",
|
||||
title: t("menu.export_sql"),
|
||||
children: [
|
||||
{ title: t("menu.generic") },
|
||||
{ title: t("menu.mysql") },
|
||||
{ title: t("menu.postgresql") },
|
||||
],
|
||||
clickHandler: () => {
|
||||
open(Modals.EXPORT_SQL)
|
||||
}
|
||||
},
|
||||
{ title: t("menu.export_orm_models"), divide: true },
|
||||
//{ title: t("menu.export_orm_models"), divide: true },
|
||||
{
|
||||
id: "menu.delete_project",
|
||||
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
|
||||
open(Modals.DELETE_DATABASE)
|
||||
}
|
||||
@@ -56,54 +66,97 @@ const Menu: React.FC<any > = ({ }) => {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.edit",
|
||||
title: t("menu.edit"),
|
||||
clickHandler: () => console.log("hello wo"),
|
||||
children: [
|
||||
{ title: t("menu.undo"), isDisabled: true },
|
||||
{ title: t("menu.redo"), isDisabled: true },
|
||||
{ title: t("menu.clear") },
|
||||
|
||||
{ id: "menu.undo", title: t("menu.undo"), isDisabled: !canUndo, clickHandler: undo },
|
||||
{ id: "menu.redo", title: t("menu.redo"), isDisabled: !canRedo, clickHandler: redo },
|
||||
{
|
||||
id: "menu.clear",
|
||||
title: t("menu.clear"), clickHandler: () => {
|
||||
const tables = database ? database.tables : [];
|
||||
deleteMultiTables(tables.map((table: TableType) => table.id))
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.view",
|
||||
title: t("menu.view"),
|
||||
children: [
|
||||
{ title: t("menu.hide_controller"), shortcut: "Ctnl + B", divide: true },
|
||||
{
|
||||
title: t("menu.zoom_on_scroll"),
|
||||
id: "menu.cardinality_style",
|
||||
title: t("menu.cardinality_style"),
|
||||
divide: true,
|
||||
children: [
|
||||
{ title: t("menu.on") },
|
||||
{ title: t("menu.off") },
|
||||
{
|
||||
id: "menu.symbolic",
|
||||
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
|
||||
title: t("menu.symbolic"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.SYMBOLIC)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "menu.numeric",
|
||||
selected: cardinalityStyle == CardinalityStyle.NUMERIC,
|
||||
title: t("menu.numeric"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.NUMERIC)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "menu.hidden",
|
||||
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
|
||||
title: t("menu.hidden"), clickHandler: () => {
|
||||
setCardinalityStyle(CardinalityStyle.HIDDEN)
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.controller_visibility",
|
||||
title: showController ? t("menu.hide_controller") : t("menu.show_controller"), shortcut: "Ctnl + B", divide: true, clickHandler: () => {
|
||||
setShowController(!showController)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "menu.theme",
|
||||
title: t("menu.theme"),
|
||||
clickHandler: () => console.log("hello wo"),
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "light",
|
||||
title: t("menu.light"),
|
||||
clickHandler: () => setTheme("light"),
|
||||
selected: resolvedTheme == "light",
|
||||
},
|
||||
{
|
||||
id: "dark",
|
||||
title: t("menu.dark"),
|
||||
clickHandler: () => setTheme("dark"),
|
||||
selected: resolvedTheme == "dark"
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.help",
|
||||
title: t("menu.help"),
|
||||
children: [
|
||||
{ title: t("menu.show_docs") },
|
||||
{ title: t("menu.join_discord") },
|
||||
|
||||
{ id: "menu.show_docs", title: t("menu.show_docs") },
|
||||
{ id: "menu.join_discord", title: t("menu.join_discord") },
|
||||
],
|
||||
},
|
||||
], [t]);
|
||||
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, setShowController, cardinalityStyle, setCardinalityStyle, resolvedTheme]);
|
||||
|
||||
return <div className="gap-1 flex">
|
||||
{
|
||||
menu.map((menuItem, index) => (
|
||||
<DropdownMenu {...menuItem} key={index} />
|
||||
<DropdownMenu {...menuItem} key={menuItem.id} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
|
||||
import { MenuDropdownProps } from "./menu-dropdown";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Check, ChevronRight } from "lucide-react";
|
||||
|
||||
import DropdownMenu from "./menu-dropdown";
|
||||
|
||||
|
||||
|
||||
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ title, children }) => {
|
||||
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ id , title, children }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<div onMouseEnter={onOpen} onMouseLeave={onClose} >
|
||||
<Popover placement="right" isOpen={isOpen} radius="sm" offset={90} shadow="sm">
|
||||
<PopoverTrigger>
|
||||
<Button className="w-full h-6 bg-transparent p-0 text-font/90" size="sm" value={"light"}>
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div onMouseEnter={onOpen} onMouseLeave={onClose} className="p-2">
|
||||
<Popover placement="right" isOpen={isOpen} radius="sm" shadow="sm" >
|
||||
<PopoverTrigger >
|
||||
<Button className="w-full h-6 bg-transparent p-0 text-font/90 " size="sm" value={"light"}>
|
||||
<span className="w-full text-left flex justify-between text-small font-normal">
|
||||
{title}
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<DropdownMenu
|
||||
<PopoverContent className="p-0 border-0 shadow-none block w-[200px] ">
|
||||
<div className="h-8">
|
||||
<DropdownMenu
|
||||
id = { id }
|
||||
children={children}
|
||||
/>
|
||||
|
||||
children={children}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -39,8 +39,14 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
|
||||
const handleAction = async (onClose: () => void) => {
|
||||
setIsLoading(true)
|
||||
actionHandler && await actionHandler();
|
||||
setIsLoading(false);
|
||||
try {
|
||||
actionHandler && await actionHandler();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
}
|
||||
}
|
||||
return (
|
||||
<HeroUiModal
|
||||
@@ -53,7 +59,7 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
|
||||
classNames={{
|
||||
base: "dark:bg-background-100",
|
||||
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md" , !closable ? "hidden" : "")
|
||||
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
|
||||
}}
|
||||
|
||||
>
|
||||
@@ -84,12 +90,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
{
|
||||
actionHandler &&
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
@@ -101,12 +110,16 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
{
|
||||
actionHandler &&
|
||||
|
||||
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ const ConnectionStatus: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (syncStatus.downloadProgress?.downloadedOperations && syncStatus.downloadProgress?.totalOperations) {
|
||||
|
||||
const progress: number = Math.round(syncStatus.downloadProgress.downloadedOperations / syncStatus.downloadProgress.totalOperations * 100);
|
||||
if (progress <= 100) {
|
||||
setDownloadProgress(progress);
|
||||
|
||||
}
|
||||
if (downloadProgress == 100 && !clearProgressHandler.current) {
|
||||
|
||||
|
||||
clearProgressHandler.current = setTimeout(() => {
|
||||
setDownloadProgress(undefined);
|
||||
clearProgressHandler.current = undefined;
|
||||
|
||||
@@ -20,8 +20,6 @@ const Navbar: React.FC<Props> = ({ }) => {
|
||||
src={`/stackrender.png`}
|
||||
width={22}
|
||||
alt="logo"
|
||||
|
||||
|
||||
/>
|
||||
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
|
||||
<div className="ml-4 w-full">
|
||||
@@ -31,8 +29,8 @@ const Navbar: React.FC<Props> = ({ }) => {
|
||||
<div className=" w-full h-full flex items-center justify-center">
|
||||
{database && <RenameDatabase database={database} />}
|
||||
</div>
|
||||
|
||||
<ConnectionStatus />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -4,15 +4,14 @@ import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
import { getDefaultTableOverlapping } from "@/utils/tables";
|
||||
import hash from "object-hash";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
|
||||
|
||||
|
||||
export const useTableToNode = (tables: TableType[]): void => {
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const tableNodes = tables.map((table: TableType) => {
|
||||
return {
|
||||
id: table.id,
|
||||
@@ -31,21 +30,23 @@ export const useTableToNode = (tables: TableType[]): void => {
|
||||
width: 224
|
||||
}
|
||||
} as Node
|
||||
})
|
||||
|
||||
}) ;
|
||||
|
||||
setNodes((nodes) => {
|
||||
return tableNodes.map((tableNode) => {
|
||||
const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id);
|
||||
|
||||
if (!node)
|
||||
return tableNode;
|
||||
else {
|
||||
const hashNode: string = hash(node.data.table as TableType);
|
||||
const hashTableNode: string = hash(tableNode.data.table as TableType);
|
||||
|
||||
return hashNode == hashTableNode ? node : tableNode;
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
}, [tables])
|
||||
|
||||
}
|
||||
+14
-6
@@ -38,7 +38,7 @@ export const en = {
|
||||
foreign_key: "Foreign Key",
|
||||
|
||||
source_table: "Source Table",
|
||||
target_table: "Target Table",
|
||||
referenced_table: "Referenced Table",
|
||||
|
||||
select_table: "Select table",
|
||||
select_field: "Select field",
|
||||
@@ -154,9 +154,11 @@ export const en = {
|
||||
clear: "Clear",
|
||||
view: "View",
|
||||
hide_controller: "Hide Controller",
|
||||
zoom_on_scroll: "Zoom on scroll",
|
||||
on: "On",
|
||||
off: "Off",
|
||||
show_controller : "Show Controller" ,
|
||||
cardinality_style: "Cardinality style",
|
||||
hidden: "Hidden",
|
||||
numeric: "Numeric",
|
||||
symbolic : "Symbolic" ,
|
||||
theme: "Theme",
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
@@ -185,8 +187,14 @@ export const en = {
|
||||
import_database: {
|
||||
title: "Import your Database",
|
||||
import: "Import",
|
||||
import_options: "Would you like to import using :"
|
||||
}
|
||||
import_options: "Would you like to import using :" ,
|
||||
import_error : "SQL Parsing Error" ,
|
||||
import_error_description : "We couldn't import your SQL because it contains invalid syntax." ,
|
||||
import_warning : "SQL Parsing Warning" ,
|
||||
import_warning_description : "Some elements couldn't be processed due to unsupported or incomplete declarations." ,
|
||||
} ,
|
||||
export_sql : "Export SQL" ,
|
||||
export_sql_header : "Export your database diagram in SQL Code"
|
||||
},
|
||||
clipboard: {
|
||||
copy: "Copy",
|
||||
|
||||
+9
-1
@@ -84,4 +84,12 @@ export const MARIADB_DUMP_EXAMPLE = "mariadb-dump -u root -p --no-data example_d
|
||||
|
||||
|
||||
export const SQLITE_DUMP_INSRUCTION = "sqlite3 [daabase_path] .schema > [output_file.sql]";
|
||||
export const SQLITE_DUMP_EXAMPLE = "sqlite3 example_db.sqlite .schema > example_db.sql";
|
||||
export const SQLITE_DUMP_EXAMPLE = "sqlite3 example_db.sqlite .schema > example_db.sql";
|
||||
|
||||
|
||||
export enum CardinalityStyle {
|
||||
HIDDEN = "HIDDEN" ,
|
||||
NUMERIC = "NUMERIC" ,
|
||||
SYMBOLIC = "SYMBOLIC"
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { Loading } from "@/components/modal/loading-modal";
|
||||
import { usePowerSync, usePowerSyncStatus } from "@powersync/react";
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +52,6 @@ const DatabasePage: React.FC = () => {
|
||||
const { open } = useModal();
|
||||
const syncStatus = usePowerSync();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { resolvedTheme } = useTheme();
|
||||
// Extract database state and operations
|
||||
@@ -66,7 +64,7 @@ const DatabasePage: React.FC = () => {
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress } = useDiagramOps();
|
||||
const { setIsConnectionInProgress, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
|
||||
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||
@@ -78,11 +76,11 @@ const DatabasePage: React.FC = () => {
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
|
||||
if (isLoading || isFetching || !(syncStatus.currentStatus as any).options.hasSynced)
|
||||
return;
|
||||
|
||||
|
||||
// no database selected , open (open database) Modal
|
||||
if (!database && databases?.length > 0) {
|
||||
open(Modals.OPEN_DATABASE, {
|
||||
@@ -95,13 +93,9 @@ const DatabasePage: React.FC = () => {
|
||||
open(Modals.CREATE_DATABASE, {
|
||||
closable: false
|
||||
});
|
||||
}
|
||||
}, [database?.id, isLoading, isFetching , (syncStatus.currentStatus as any).options.hasSynced])
|
||||
}
|
||||
}, [database?.id, isLoading, isFetching, (syncStatus.currentStatus as any).options.hasSynced])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isSwitchingDatabase) {
|
||||
setNodes([]);
|
||||
@@ -121,7 +115,6 @@ const DatabasePage: React.FC = () => {
|
||||
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
|
||||
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
|
||||
|
||||
|
||||
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
|
||||
|
||||
// Check if both fields have the same type (valid relationship)
|
||||
@@ -135,8 +128,6 @@ const DatabasePage: React.FC = () => {
|
||||
|
||||
} as RelationshipInsertType);
|
||||
|
||||
// Add edge to the diagram
|
||||
//setEdges((eds) => addEdge(connection, eds));
|
||||
} else {
|
||||
// Show error toast if invalid relationship
|
||||
addToast({
|
||||
@@ -158,10 +149,11 @@ const DatabasePage: React.FC = () => {
|
||||
) as NodePositionChange[];
|
||||
|
||||
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
||||
|
||||
// Save new positions to the database
|
||||
|
||||
if (nodePositionChanges.length > 0)
|
||||
await updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||
|
||||
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||
id: change.id,
|
||||
posX: change.position?.x,
|
||||
posY: change.position?.y
|
||||
@@ -177,7 +169,6 @@ const DatabasePage: React.FC = () => {
|
||||
// Called when edges (relationships) change
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
||||
|
||||
// Delete relationships from database
|
||||
if (edgeRemoveChanges.length > 0) {
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
||||
@@ -215,7 +206,6 @@ const DatabasePage: React.FC = () => {
|
||||
const { isOverlapping, puls } = useOverlappingTables(tables);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="w-full h-screen flex relative overflow-hidden">
|
||||
@@ -239,8 +229,8 @@ const DatabasePage: React.FC = () => {
|
||||
onConnect={onConnect}
|
||||
defaultEdgeOptions={{
|
||||
type: 'relationship-edge',
|
||||
animated: false
|
||||
}}
|
||||
|
||||
panOnDrag={true}
|
||||
zoomOnScroll={true}
|
||||
nodeTypes={nodeTypes}
|
||||
@@ -249,6 +239,7 @@ const DatabasePage: React.FC = () => {
|
||||
minZoom={0.10}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
|
||||
>
|
||||
<Controls
|
||||
position="bottom-center"
|
||||
@@ -300,29 +291,32 @@ const DatabasePage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
}
|
||||
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
||||
<defs>
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="start" />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="start" selected />
|
||||
{
|
||||
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
|
||||
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
||||
<defs>
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="end" />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="end" selected />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
|
||||
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="start" />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="start" selected />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="end" />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="end" selected />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
|
||||
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default DatabasePage;
|
||||
export default DatabasePage;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
import React, { useState } from "react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { ResizableBox as ResizableBoxRaw } from 'react-resizable';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { GripVertical } from "lucide-react";
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
|
||||
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
|
||||
|
||||
@@ -14,29 +16,56 @@ interface Props {
|
||||
|
||||
const DBController: React.FC<Props> = ({ }) => {
|
||||
const [width, setWidth] = useState<number>(512);
|
||||
const { showController } = useDiagramOps()
|
||||
const [show, setShow] = useState<boolean>(!showController);
|
||||
const [style, setStyle] = useState<any>(undefined);
|
||||
const onResize = (event: any, params: any) => {
|
||||
setWidth(params.size.width);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!showController) {
|
||||
setWidth(0);
|
||||
setStyle({
|
||||
transition: 'width 0.5s',
|
||||
})
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
width={width}
|
||||
onResize={onResize} className="min-h-full overflow-visible "
|
||||
minConstraints={[512]}
|
||||
axis="x"
|
||||
handle={
|
||||
} else if (showController) {
|
||||
setShow(true);
|
||||
setTimeout(() => setWidth(512))
|
||||
;
|
||||
|
||||
setTimeout(() => {
|
||||
setStyle(undefined);
|
||||
}, 500)
|
||||
}
|
||||
}, [showController])
|
||||
|
||||
if (show)
|
||||
return (
|
||||
|
||||
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary-300 active:border-primary-400 transition-colors duration-200 ">
|
||||
<ResizableBox
|
||||
width={width}
|
||||
onResize={onResize} className="min-h-full overflow-hidden "
|
||||
minConstraints={[512]}
|
||||
axis="x"
|
||||
style={style}
|
||||
handle={
|
||||
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary-300 active:border-primary-400 transition-colors duration-200 ">
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="min-w-[512px] w-full h-full bg-background dark:bg-background-50 border-r p-2 pt-[52px] border-default-100 dark:border-divider">
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="min-w-[512px] w-full h-full bg-background dark:bg-background-50 border-r p-2 pt-[52px] border-default-100 dark:border-divider">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
</ResizableBox>
|
||||
)
|
||||
</ResizableBox>
|
||||
)
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
export default React.memo(DBController)
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
<div className="w-full space-y-1">
|
||||
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm ">
|
||||
<FileMinus2 className="size-4" />
|
||||
{t(" .target_table")}
|
||||
{t("db_controller.referenced_table")}
|
||||
|
||||
</label>
|
||||
|
||||
|
||||
+14
-9
@@ -10,7 +10,7 @@ import CreateRelationshipForm from "../../modals/create-relationship-modal";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
@@ -39,13 +39,17 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
useEffect(() => {
|
||||
if (focusedRelationshipId) {
|
||||
setSelectedRelationship(new Set([focusedRelationshipId]) as any);
|
||||
const accordionItem = document.getElementById(focusedRelationshipId)
|
||||
if (accordionItem)
|
||||
accordionItem?.scrollIntoView({
|
||||
behavior: 'smooth', block: 'center'
|
||||
})
|
||||
}
|
||||
|
||||
}, [focusedRelationshipId]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
open(Modals.CREATE_RELATIONSHIP, {
|
||||
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
|
||||
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -66,12 +70,12 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
}
|
||||
}, [nameRef, allRelationships]);
|
||||
|
||||
const selectedRelationshipId = selectedRelationship.values().next().value;
|
||||
const selectedRelationshipId = selectedRelationship.values().next().value;
|
||||
|
||||
|
||||
const collapseAll = useCallback(() => {
|
||||
setSelectedRelationship(new Set([])) ;
|
||||
} , [])
|
||||
setSelectedRelationship(new Set([]));
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
@@ -84,8 +88,8 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
variant="light"
|
||||
className="size-8 p-0 text-icon hover:text-font/90"
|
||||
isIconOnly
|
||||
onPressEnd={collapseAll}
|
||||
|
||||
onPressEnd={collapseAll}
|
||||
|
||||
|
||||
>
|
||||
<ListCollapse className="size-4" />
|
||||
@@ -108,7 +112,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
onKeyUp={searchRelationships}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
}}
|
||||
|
||||
/>
|
||||
@@ -138,6 +142,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
{relationships.map((relationship: RelationshipType) => (
|
||||
<AccordionItem
|
||||
key={relationship.id}
|
||||
id={relationship.id}
|
||||
aria-label={relationship.id}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200 h-12 dark:hover:bg-background",
|
||||
|
||||
@@ -18,38 +18,29 @@ import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
|
||||
interface SqlPreviewProps {
|
||||
tableFilterIds ? : string[]
|
||||
tableFilterIds?: string[]
|
||||
}
|
||||
|
||||
|
||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
const { database : currentDatabase } = useDatabase();
|
||||
|
||||
|
||||
const database = useMemo(() => {
|
||||
const { database: currentDatabase } = useDatabase();
|
||||
const database = useMemo(() => {
|
||||
if ( !tableFilterIds )
|
||||
return currentDatabase ;
|
||||
return {
|
||||
...currentDatabase ,
|
||||
tables : currentDatabase?.tables.filter((table : TableType) => tableFilterIds?.includes(table.id)) ,
|
||||
relationships : currentDatabase?.relationships.filter((relationship : RelationshipType) =>
|
||||
tableFilterIds?.includes( relationship.sourceTableId) || tableFilterIds?.includes(relationship.targetFieldId)
|
||||
),
|
||||
} as DatabaseType ;
|
||||
} , [currentDatabase ,tableFilterIds ])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
...currentDatabase,
|
||||
tables: currentDatabase?.tables.filter((table: TableType) => tableFilterIds?.includes(table.id)),
|
||||
relationships: currentDatabase?.relationships.filter((relationship: RelationshipType) =>
|
||||
tableFilterIds?.includes(relationship.sourceTableId) || tableFilterIds?.includes(relationship.targetFieldId)
|
||||
),
|
||||
} as DatabaseType;
|
||||
}, [currentDatabase, tableFilterIds])
|
||||
|
||||
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
|
||||
console.log ( tableFilterIds ) ;
|
||||
|
||||
useEffect(() => {
|
||||
if (circularDependency)
|
||||
addToast({
|
||||
@@ -62,26 +53,26 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
if (circularDependency)
|
||||
return <CircularDependencyAlert error={circularDependency} />
|
||||
|
||||
else
|
||||
return (
|
||||
<div className="flex w-full h-full relative">
|
||||
<div className="flex w-full h-full relative ">
|
||||
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
|
||||
<Clipboard
|
||||
text={sqlCode}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
<CodeMirror
|
||||
defaultValue={sqlCode}
|
||||
value={sqlCode}
|
||||
className="flex flex-1 w-full "
|
||||
extensions={[sql()]}
|
||||
readOnly
|
||||
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
|
||||
/>
|
||||
}
|
||||
|
||||
<CodeMirror
|
||||
defaultValue={sqlCode}
|
||||
value={sqlCode}
|
||||
className="flex flex-1 w-full "
|
||||
extensions={[sql()]}
|
||||
readOnly
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -89,4 +80,4 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
|
||||
|
||||
export default React.memo(SqlPreview) ;
|
||||
export default React.memo(SqlPreview);
|
||||
+8
-2
@@ -11,6 +11,7 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
|
||||
import Autocomplete from "@/components/auto-complete/auto-complete";
|
||||
import ToggleButton from "@/components/toggle/toggle";
|
||||
import FieldSetting from "./field-setting";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
interface Props {
|
||||
field: FieldType
|
||||
}
|
||||
@@ -22,7 +23,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const { grouped_data_types } = useDatabaseOperations();
|
||||
const { grouped_data_types, data_types } = useDatabaseOperations();
|
||||
const { editField } = useDatabaseOperations();
|
||||
|
||||
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
|
||||
@@ -50,13 +51,18 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
} as FieldType);
|
||||
}
|
||||
const updateFieldType = (key: Key | null) => {
|
||||
|
||||
|
||||
const dataType: DataType | undefined = data_types.find((dataTypes: DataType) => dataTypes.id == key);
|
||||
if (!dataType)
|
||||
return;
|
||||
|
||||
if (key != null) {
|
||||
editField({
|
||||
id: field.id,
|
||||
typeId: key
|
||||
} as FieldType);
|
||||
setSelectedType(key as string | undefined);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -87,7 +87,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
id: field.id,
|
||||
note: note,
|
||||
} as FieldInsertType)
|
||||
}, [field])
|
||||
}, [field , note]) ;
|
||||
|
||||
const toggleUnqiue = useCallback((value: boolean) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
@@ -569,7 +570,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
base: "max-w-xs",
|
||||
input: "resize-y min-h-[60px] max-h-[180px]",
|
||||
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
label: "text-font/90 group-data-[focus=true]:text-font/70"
|
||||
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70 "
|
||||
}} />
|
||||
<hr className="border-divider" />
|
||||
<Button
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table, keys })
|
||||
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")}
|
||||
classNames={{
|
||||
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
label: "text-font/90 group-data-[focus=true]:text-font/70"
|
||||
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70"
|
||||
}}
|
||||
value={note}
|
||||
onValueChange={setNote}
|
||||
|
||||
+11
-17
@@ -21,16 +21,13 @@ export interface TableAccordionHeaderProps {
|
||||
|
||||
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const { editTable, deleteTable, createField, createTable, createIndex , getInteger } = useDatabaseOperations();
|
||||
|
||||
const { editTable, deleteTable, createField, createTable, createIndex, getInteger } = useDatabaseOperations();
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { focusOnTable } = useDiagramOps();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTableName(table.name);
|
||||
}, [table.name])
|
||||
@@ -40,12 +37,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
setEditMode(false);
|
||||
}, [tableName])
|
||||
|
||||
const onDeleteTable = async () => {
|
||||
const onDeleteTable = useCallback(async () => {
|
||||
|
||||
deleteTable(table.id)
|
||||
setPopOverOpen(false);
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
const addField = () => {
|
||||
const addField = useCallback(() => {
|
||||
setPopOverOpen(false)
|
||||
createField({
|
||||
id: v4(),
|
||||
@@ -53,30 +51,26 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(table.fields),
|
||||
nullable: true,
|
||||
typeId : getInteger()?.id
|
||||
typeId: getInteger()?.id
|
||||
})
|
||||
}
|
||||
}, [table, getInteger])
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
const addIndex = useCallback((event: any) => {
|
||||
setPopOverOpen(false)
|
||||
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${table.indices.length + 1}`,
|
||||
unique: true,
|
||||
tableId: table.id
|
||||
} as IndexInsertType);
|
||||
}
|
||||
}, [table])
|
||||
|
||||
const duplicate = async () => {
|
||||
const duplicate = useCallback(async () => {
|
||||
setPopOverOpen(false)
|
||||
const clonedTable: TableType = cloneTable(table);
|
||||
await createTable(clonedTable)
|
||||
focusOnTable(clonedTable.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}, [table])
|
||||
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center border-l-[6px] border-default"
|
||||
|
||||
@@ -64,11 +64,10 @@ const TablesController: React.FC = ({ }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTableId) {
|
||||
|
||||
setSelectedTable(new Set([focusedTableId]) as any);
|
||||
setShowSqlPreview(false) ;
|
||||
const accordionItem = document.getElementById(focusedTableId)
|
||||
if (accordionItem)
|
||||
|
||||
accordionItem?.scrollIntoView({
|
||||
behavior: 'smooth', block: 'center'
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
placeholder={t("modals.db_name")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -106,16 +106,14 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
className="w-full text-font/90 border-1 border-divider"
|
||||
>
|
||||
<SquareMenu className="size-4" /> Check examples
|
||||
|
||||
<SquareMenu className="size-4 text-icon" /> Check examples
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
|
||||
className="w-full text-font/90 border-1 border-divider"
|
||||
>
|
||||
<span className="underline">
|
||||
Empty Diagram
|
||||
|
||||
@@ -71,9 +71,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
|
||||
|
||||
|
||||
const addRelationship = useCallback(() => {
|
||||
|
||||
const id: string = v4();
|
||||
|
||||
createRelationship({
|
||||
...relationship,
|
||||
id,
|
||||
@@ -109,7 +107,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
|
||||
<FileMinus2 className="size-4" />
|
||||
{t("db_controller.target_table")}
|
||||
{t("db_controller.referenced_table")}
|
||||
</label>
|
||||
|
||||
<Autocomplete
|
||||
|
||||
@@ -29,9 +29,8 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
className="min-w-[560px]"
|
||||
actionHandler={onDelete}
|
||||
variant="danger"
|
||||
|
||||
>
|
||||
<p className="text-font/90">
|
||||
<p className="text-font/90 text-sm font-medium">
|
||||
{t("modals.delete_database_content")}
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
import Modal, { ModalProps } from "@/components/modal/modal";
|
||||
|
||||
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SqlPreview from "../db-controller/sql-preview";
|
||||
|
||||
const ExportSqlModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.export_sql")}
|
||||
className="min-w-[960px] h-[90vh]"
|
||||
header={t("modals.export_sql_header")}
|
||||
>
|
||||
<div className="h-full max-h-[72vh]">
|
||||
<SqlPreview />
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export default React.memo(ExportSqlModal)
|
||||
@@ -7,112 +7,120 @@ import { sql } from '@codemirror/lang-sql';
|
||||
import { useTheme } from "next-themes";
|
||||
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType, MARIADB_DUMP_EXAMPLE, MARIADB_DUMP_INSTRUCTIONS, MYSQL_DUMP_EXAMPLE, MYSQL_DUMP_INSTRUCTIONS, PG_DUMP_EXAMPLE, PG_DUMP_INSTRUCTIONS, SQLITE_DUMP_EXAMPLE, SQLITE_DUMP_INSRUCTION } from "@/lib/database";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { CheckboxGroup } from "@heroui/react";
|
||||
import { Alert, CheckboxGroup, cn } from "@heroui/react";
|
||||
import OptionCheckbox from "@/components/checkbox/option-checkbox";
|
||||
import { Code } from "lucide-react";
|
||||
import { Code as CodeSection } from "@heroui/react";
|
||||
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
|
||||
import Clipboard from "@/components/clipboard/clipboard";
|
||||
import { Trans } from 'react-i18next';
|
||||
import { Parser } from "node-sql-parser";
|
||||
import { DatabaseInsertType } from "@/lib/schemas/database-schema";
|
||||
import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { adjustTablesPositions } from "@/utils/tables";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
|
||||
const options: ImportDatabaseOption[] = [{
|
||||
dialect: DatabaseDialect.POSTGRES,
|
||||
methods: [{
|
||||
id: "pg_dump",
|
||||
name: "pg_dump",
|
||||
icon: <Code className="size-3.5 " />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: PG_DUMP_INSTRUCTIONS,
|
||||
example: PG_DUMP_EXAMPLE
|
||||
|
||||
}, {
|
||||
id: "pg_admin",
|
||||
name: "Pg Admin",
|
||||
logo: "/postgresql_logo_small.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
|
||||
}]
|
||||
}, {
|
||||
dialect: DatabaseDialect.MYSQL,
|
||||
methods: [{
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MYSQL_DUMP_INSTRUCTIONS,
|
||||
example: MYSQL_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "workbench",
|
||||
name: "MySQL Workbench",
|
||||
logo: "/mysql_logo_small.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}
|
||||
, {
|
||||
dialect: DatabaseDialect.MARIADB,
|
||||
methods: [{
|
||||
id: "mariadb-dump",
|
||||
name: "mariadb-dump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MARIADB_DUMP_INSTRUCTIONS,
|
||||
example: MARIADB_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "heidisql",
|
||||
name: "HeidiSQL",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
logo: "/heidisqlL_logo.png",
|
||||
}, {
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MYSQL_DUMP_INSTRUCTIONS,
|
||||
example: MYSQL_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "workbench",
|
||||
name: "MySQL Workbench",
|
||||
logo: "/mysql_logo_small.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}],
|
||||
|
||||
}, {
|
||||
dialect: DatabaseDialect.SQLITE,
|
||||
methods: [{
|
||||
id: "sqlite3",
|
||||
name: "Sqlite3",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: SQLITE_DUMP_INSRUCTION,
|
||||
example: SQLITE_DUMP_EXAMPLE
|
||||
}, {
|
||||
id: "dbbrowser",
|
||||
name: "DB Browser",
|
||||
logo: "/dbbrowser.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}
|
||||
];
|
||||
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const options: ImportDatabaseOption[] = useMemo(() => [{
|
||||
dialect: DatabaseDialect.POSTGRES,
|
||||
methods: [{
|
||||
id: "pg_dump",
|
||||
name: "pg_dump",
|
||||
icon: <Code className="size-3.5 " />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: PG_DUMP_INSTRUCTIONS,
|
||||
example: PG_DUMP_EXAMPLE
|
||||
|
||||
}, {
|
||||
id: "pg_admin",
|
||||
name: "Pg Admin",
|
||||
logo: "/postgresql_logo.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
|
||||
}]
|
||||
}, {
|
||||
dialect: DatabaseDialect.MYSQL,
|
||||
methods: [{
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MYSQL_DUMP_INSTRUCTIONS,
|
||||
example: MYSQL_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "workbench",
|
||||
name: "MySQL Workbench",
|
||||
logo: "/mysql_logo.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}
|
||||
, {
|
||||
dialect: DatabaseDialect.MARIADB,
|
||||
methods: [{
|
||||
id: "mariadb-dump",
|
||||
name: "mariadb-dump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MARIADB_DUMP_INSTRUCTIONS,
|
||||
example: MARIADB_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "heidisql",
|
||||
name: "HeidiSQL",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
logo: "/heidisqlL_logo.png",
|
||||
}, {
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: MYSQL_DUMP_INSTRUCTIONS,
|
||||
example: MYSQL_DUMP_EXAMPLE,
|
||||
}, {
|
||||
id: "workbench",
|
||||
name: "MySQL Workbench",
|
||||
logo: "/mysql_logo.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}],
|
||||
|
||||
}, {
|
||||
dialect: DatabaseDialect.SQLITE,
|
||||
methods: [{
|
||||
id: "sqlite3",
|
||||
name: "Sqlite3",
|
||||
icon: <Code className="size-3.5" />,
|
||||
type: ImportMethodType.DUMP,
|
||||
instruction: SQLITE_DUMP_INSRUCTION,
|
||||
example: SQLITE_DUMP_EXAMPLE
|
||||
}, {
|
||||
id: "dbbrowser",
|
||||
name: "DB Browser",
|
||||
logo: "/dbbrowser.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}
|
||||
], [t])
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { database , isLoading , isSwitchingDatabase } = useDatabase();
|
||||
const { data_types, importDatabase } = useDatabaseOperations();
|
||||
const { database, isLoading, isSwitchingDatabase } = useDatabase();
|
||||
const { data_types, importDatabase } = useDatabaseOperations();
|
||||
const [sqlCode, setSqlCode] = useState<string>("");
|
||||
const [parsedDatabase, setParsedDatabase] = useState<any | undefined>(undefined)
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
|
||||
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
|
||||
}, [database]);
|
||||
|
||||
const [selectedMethodId, setSelectedMethodId] = useState<string[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMethodId(currentOption ? [currentOption.methods[0].id] : [])
|
||||
}, [currentOption])
|
||||
@@ -127,17 +135,56 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId?.[0]) as ImportDatabaseMethod;
|
||||
}, [selectedMethodId, currentOption])
|
||||
|
||||
const validateSql = useCallback((code: string) => {
|
||||
try {
|
||||
const parsedDatabase = SqlToDatabase(code, data_types, database?.dialect as DatabaseDialect);
|
||||
setParsedDatabase(parsedDatabase);
|
||||
setError(false);
|
||||
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
setParsedDatabase(undefined)
|
||||
}
|
||||
setSqlCode(code)
|
||||
|
||||
}, [database?.dialect, data_types])
|
||||
|
||||
|
||||
|
||||
const onImport = useCallback(async () => {
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
const nodes: Node[] = parsedDatabase.tables.map((table: TableInsertType) => ({
|
||||
id: table.id,
|
||||
data: {
|
||||
table
|
||||
}
|
||||
}))
|
||||
|
||||
const adjustedTables = await adjustTablesPositions(nodes, parsedDatabase.relationships);
|
||||
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indices);
|
||||
|
||||
onOpenChange && onOpenChange(false);
|
||||
|
||||
setTimeout(() => {
|
||||
fitView({
|
||||
duration: 500
|
||||
});
|
||||
}, 300);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setError(true);
|
||||
rej()
|
||||
}
|
||||
})
|
||||
}, [parsedDatabase]);
|
||||
|
||||
|
||||
const { tables, relationships, indices } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect);
|
||||
await importDatabase(tables, relationships, indices);
|
||||
onOpenChange && onOpenChange(false);
|
||||
|
||||
}, [sqlCode, database?.dialect, data_types]);
|
||||
|
||||
|
||||
if (isLoading || isSwitchingDatabase)
|
||||
return ;
|
||||
return;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -147,6 +194,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
actionName={t("modals.import_database.import")}
|
||||
className="min-w-[860px] max-w-[860px]"
|
||||
actionHandler={onImport}
|
||||
isDisabled={!parsedDatabase}
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="w-full ">
|
||||
@@ -188,7 +236,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
<div>
|
||||
{t("import.run_command")}
|
||||
</div>
|
||||
<CodeSection color="default" radius="md" className="selectable border-1 py-0.5 bg-default w-full text-font/90 flex items-center justify-between dark:border-divider" >
|
||||
<CodeSection color="default" radius="md" className="selectable border-1 border-divider py-0.5 bg-background w-full text-font/90 flex items-center justify-between dark:border-divider dark:bg-transparent" >
|
||||
{selectedImportMethod.instruction}
|
||||
<Clipboard text={selectedImportMethod.instruction} />
|
||||
</CodeSection>
|
||||
@@ -196,7 +244,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
<div>
|
||||
{t("import.example")}
|
||||
</div>
|
||||
<CodeSection color="default" radius="md" className="selectable border-1 py-0.5 bg-default w-full text-font/90 flex items-center justify-between dark:border-divider" >
|
||||
<CodeSection color="default" radius="md" className="selectable border-1 border-divider py-0.5 bg-background w-full text-font/90 flex items-center justify-between dark:border-divider dark:bg-transparent" >
|
||||
{selectedImportMethod.example}
|
||||
<Clipboard text={selectedImportMethod.example} />
|
||||
</CodeSection>
|
||||
@@ -221,8 +269,8 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
<Trans i18nKey={`import.${selectedImportMethod.id}.step${index + 1}`} components={{
|
||||
bold: <span className="font-medium" />,
|
||||
code: <CodeSection
|
||||
className="border-1 bg-default text-font/90 px-1 h-7 mx-0.5 dark:border-divider"
|
||||
color="default"
|
||||
className="border-1 bg-default text-font/90 px-1 h-7 mx-0.5 dark:border-divider "
|
||||
color="primary"
|
||||
size="sm"
|
||||
/>
|
||||
}} />
|
||||
@@ -235,16 +283,40 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="flex flex-1 ">
|
||||
|
||||
<div className="flex flex-1 flex-col ">
|
||||
<ReactCodeMirror
|
||||
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
|
||||
className={
|
||||
cn("flex w-full border-1 rounded-md border-divider overflow-hidden ",
|
||||
(error || parsedDatabase?.errors?.length > 0) ? "min-h-[360px] max-h-[360px]" : "min-h-[424px] max-h-[424px]"
|
||||
)
|
||||
}
|
||||
extensions={[sql()]}
|
||||
value={sqlCode}
|
||||
onChange={setSqlCode}
|
||||
onChange={validateSql}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
{
|
||||
error &&
|
||||
<Alert
|
||||
color="danger"
|
||||
description={t("modals.import_database.import_error_description")}
|
||||
title={t("modals.import_database.import_error")}
|
||||
variant={resolvedTheme == "dark" ? "solid" : "faded"}
|
||||
/>
|
||||
}
|
||||
{
|
||||
(parsedDatabase?.errors && parsedDatabase?.errors?.length > 0) &&
|
||||
<Alert
|
||||
color="warning"
|
||||
description={t("modals.import_database.import_warning_description")}
|
||||
title={t("modals.import_database.import_warning")}
|
||||
|
||||
variant={resolvedTheme == "dark" ? "solid" : "faded"}
|
||||
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { Image, Selection, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@heroui/react";
|
||||
import { Image, Pagination, Selection, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@heroui/react";
|
||||
import { Key, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -16,14 +16,20 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
const { isOpen, onOpenChange } = props;
|
||||
const { t } = useTranslation();
|
||||
const { databases, currentDatabaseId } = useDatabase();
|
||||
|
||||
const { switchDatabase } = useDatabaseOperations();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(
|
||||
(() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined)
|
||||
);
|
||||
|
||||
const openDatabase = () => {
|
||||
switchDatabase(selectedDatabase.currentKey) ;
|
||||
onOpenChange && onOpenChange(false) ;
|
||||
switchDatabase(selectedDatabase.currentKey);
|
||||
onOpenChange && onOpenChange(false);
|
||||
}
|
||||
|
||||
const onSelectionChange = (selection: Selection) => {
|
||||
if ((selection as any).currentKey != selectedDatabase?.currentKey)
|
||||
setSelectedDatabase(selection)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,26 +39,29 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.open_database")}
|
||||
actionName={t("modals.open")}
|
||||
className="min-w-[860px]"
|
||||
className="min-w-[820px]"
|
||||
actionHandler={openDatabase}
|
||||
header={t("modals.open_database_header")}
|
||||
isDisabled={!selectedDatabase?.size}
|
||||
|
||||
>
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example static collection table"
|
||||
color={"primary"}
|
||||
selectionMode="single"
|
||||
selectedKeys={selectedDatabase}
|
||||
onSelectionChange={setSelectedDatabase}
|
||||
onSelectionChange={onSelectionChange}
|
||||
|
||||
classNames={{
|
||||
wrapper: "min-h-[360px] shadow-none border-1 border-divider rounded-sm",
|
||||
th: "dark:bg-background",
|
||||
tr: "hover:bg-default-100 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 "
|
||||
wrapper: "shadow-sm border-1 border-divider dark:bg-background-100 rounded-sm",
|
||||
th: "dark:bg-background font-bold text-center rounded-sm",
|
||||
tr: " hover:bg-default-100 h-10 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 ",
|
||||
base: "max-h-[520px] overflow-y-scroll ",
|
||||
td: "items-center text-center" ,
|
||||
}}
|
||||
>
|
||||
<TableHeader className="rounded-sm">
|
||||
<TableColumn >Dialect</TableColumn>
|
||||
<TableColumn>Dialect</TableColumn>
|
||||
<TableColumn>Name</TableColumn>
|
||||
<TableColumn>Created at</TableColumn>
|
||||
<TableColumn>Tables</TableColumn>
|
||||
@@ -62,14 +71,16 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
databases.map((database: DatabaseType) => (
|
||||
<TableRow key={database.id}>
|
||||
<TableCell>
|
||||
<Image
|
||||
src={getDatabaseByDialect(database.dialect).small_logo}
|
||||
width={24}
|
||||
radius="none"
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src={getDatabaseByDialect(database.dialect).small_logo}
|
||||
width={24}
|
||||
radius="none"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold">{database.name}</TableCell>
|
||||
<TableCell>{
|
||||
<TableCell className="font-semibold ">{database.name}</TableCell>
|
||||
<TableCell className="text-font/70">{
|
||||
new Date(database.createdAt as string).toLocaleString("en-US")
|
||||
}</TableCell>
|
||||
<TableCell>{database.numOfTables}</TableCell>
|
||||
|
||||
@@ -123,37 +123,37 @@ const Field: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 ",
|
||||
!showHandles ? "invisible" : "visible"
|
||||
)} >
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id={LEFT_PREFIX + field.id}
|
||||
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
|
||||
id={RIGHT_PREFIX + field.id}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 ",
|
||||
!isConnectionInProgress ? "invisible" : "visible"
|
||||
!showHandles ? "invisible" : "visible"
|
||||
)} >
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id={LEFT_PREFIX + field.id}
|
||||
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
|
||||
id={RIGHT_PREFIX + field.id}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 h-full ",
|
||||
!isConnectionInProgress ? "invisible" : "visible"
|
||||
)}>
|
||||
<Handle
|
||||
id={`${TARGET_PREFIX}${field.id}`}
|
||||
className={
|
||||
true
|
||||
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
|
||||
: `!invisible`
|
||||
'absolute left-0 top-0 h-full w-full transform-none rounded-none border-none opacity-0'
|
||||
}
|
||||
position={Position.Left}
|
||||
type="target"
|
||||
|
||||
@@ -57,9 +57,10 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
}, [table])
|
||||
|
||||
|
||||
|
||||
|
||||
const fields: React.ReactNode[] = useMemo(() => {
|
||||
return table.fields.map((field: FieldType) => {
|
||||
|
||||
const highlight: boolean = highlightedEdges.find((edge: any) =>
|
||||
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
|
||||
(edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null;
|
||||
@@ -78,6 +79,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
setShowMore((previousShowMore) => !previousShowMore);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
@@ -183,21 +185,16 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
|
||||
)
|
||||
};
|
||||
|
||||
export default React.memo(Table , (previousState: any, newState: any) => {
|
||||
|
||||
|
||||
|
||||
export default React.memo(Table , (previousState: any, newState: any) => {
|
||||
// compare the previous state to the new one just to prevent re-rendering when we drag a table
|
||||
const previousStateHash: string = hash(previousState.data);
|
||||
const newStateHash: string = hash(newState.data);
|
||||
return previousStateHash == newStateHash && previousState.selected == newState.selected;
|
||||
});
|
||||
|
||||
/*
|
||||
,
|
||||
|
||||
|
||||
*/
|
||||
|
||||
@@ -12,19 +12,28 @@ interface Props { children: React.ReactNode };
|
||||
const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const udpateDbFlag = useRef(false);
|
||||
const { database } = useDatabase() as { database: DatabaseType };
|
||||
const { executeDbDiffOps } = useDatabaseOperations();
|
||||
const { database , isLoading , isFetching } = useDatabase() ;
|
||||
const { executeDbDiffOps } = useDatabaseOperations();
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
|
||||
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo, reset }] = useUndo<DatabaseType>(database as DatabaseType);
|
||||
|
||||
useEffect(() => {
|
||||
if (datatbaseState.present && database) {
|
||||
if (database)
|
||||
reset(database);
|
||||
}, [database?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (database && datatbaseState.present && !isLoading && !isFetching) {
|
||||
udpateDbFlag.current = false;
|
||||
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
|
||||
|
||||
const presentHash: string | undefined = hash(datatbaseState.present, { algorithm: 'sha1' });
|
||||
const databaseHash: string = hash(database, { algorithm: 'sha1' });
|
||||
if (presentHash != databaseHash)
|
||||
|
||||
if (presentHash != databaseHash) {
|
||||
|
||||
set(database);
|
||||
}
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
@@ -50,13 +59,14 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! database || !datatbaseState.present )
|
||||
return ;
|
||||
if (!database || !datatbaseState.present)
|
||||
return;
|
||||
const normalizedDatabase = normalizeDatabase(database);
|
||||
|
||||
const normalizedPresent = normalizeDatabase(datatbaseState.present);
|
||||
const differences = compare(normalizedDatabase, normalizedPresent);
|
||||
|
||||
//console.log ( differences )
|
||||
|
||||
if (differences && differences.length > 0) {
|
||||
setIsProcessing(true);
|
||||
@@ -65,11 +75,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// console.log ( operations) ;
|
||||
await executeDbDiffOps(operations)
|
||||
setIsProcessing(false);
|
||||
}
|
||||
catch (error) {
|
||||
set(database);
|
||||
console.log ( error ) ;
|
||||
/// set(database);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -22,9 +22,7 @@ interface DatabaseDataContextType {
|
||||
isFetching : boolean ,
|
||||
isSwitchingDatabase: boolean,
|
||||
getField: (tableId: string, id: string) => FieldType | undefined,
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +40,7 @@ interface DatabaseOperationsContextType {
|
||||
createTable: (table: TableInsertType) => Promise<void>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<void>,
|
||||
updateTablePositions: (tables: TableInsertType[]) => Promise<void>,
|
||||
updateTablePositions: (tables: TableInsertType[] ) => Promise<void>,
|
||||
deleteMultiTables: (ids: string[]) => Promise<void>
|
||||
// field operations
|
||||
createField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
@@ -64,7 +62,7 @@ interface DatabaseOperationsContextType {
|
||||
executeDbDiffOps: (operations: DBDiffOperation[]) => void,
|
||||
|
||||
// insert databse tables , relationships , indices in one operation
|
||||
importDatabase : (tables : TableInsertType[] , relationships : RelationshipInsertType[] , indices : IndexInsertType[]) => Promise<void> ;
|
||||
importDatabase : (tables : TableInsertType[] , relationships : RelationshipInsertType[] , indices : IndexInsertType[]) => Promise<any> ;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useQuery } from "@powersync/react";
|
||||
import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
import { asc, count, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import { QueryResult, Transaction } from "@powersync/web";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipInsertType, relationships } from "@/lib/schemas/relationship-schema";
|
||||
import DatabaseHistoryProvider from "../database-history/database-history-provider";
|
||||
@@ -16,7 +16,7 @@ import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
|
||||
import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { Modifiers } from "@/lib/field";
|
||||
import { deleteFieldsWithCascade, deleteTablesWithCascade } from "@/utils/cascade";
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
type: true
|
||||
}
|
||||
},
|
||||
|
||||
indices: {
|
||||
orderBy: asc(indices.createdAt),
|
||||
with: {
|
||||
@@ -55,6 +56,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
relationships: {
|
||||
with: {
|
||||
sourceTable: true,
|
||||
@@ -66,7 +68,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
), [], { runQueryOnce: false },
|
||||
);
|
||||
|
||||
|
||||
@@ -84,6 +86,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
else
|
||||
database = undefined as any;
|
||||
|
||||
|
||||
// Fetch all data types
|
||||
let { data: data_types, isLoading: loadingDataTypes, isFetching: fetchingDatatypes } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany({
|
||||
@@ -149,11 +152,13 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
}, [db, currentDatabaseId]);
|
||||
}, [db, currentDatabaseId, database]);
|
||||
|
||||
const editTable = useCallback(async (table: TableInsertType): Promise<QueryResult> => {
|
||||
return await db.update(tables).set(table).where(eq(tables.id, table.id));
|
||||
@@ -162,9 +167,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const deleteTable = useCallback(async (id: string): Promise<void> => {
|
||||
if (currentDatabaseId) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(tables).where(eq(tables.id, id));
|
||||
await updateDbNumTables(currentDatabaseId, tx)
|
||||
})
|
||||
await deleteFieldsWithCascade([id], tx);
|
||||
await updateDbNumTables(currentDatabaseId, tx);
|
||||
});
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
@@ -174,9 +179,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const deleteMultiTables = useCallback(async (ids: string[]): Promise<void> => {
|
||||
if (currentDatabaseId) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(tables).where(inArray(tables.id, ids));;
|
||||
await updateDbNumTables(currentDatabaseId, tx)
|
||||
})
|
||||
await deleteTablesWithCascade(ids, tx);
|
||||
await updateDbNumTables(currentDatabaseId, tx);
|
||||
});
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
@@ -195,8 +200,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
// Delete field and its related relationships
|
||||
const deleteField = useCallback(async (id: string): Promise<void> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, id), eq(relationships.targetFieldId, id)));
|
||||
await tx.delete(fields).where(eq(fields.id, id));
|
||||
await deleteFieldsWithCascade([id], tx);
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
@@ -271,85 +275,131 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}, [db]);
|
||||
|
||||
// Update table positions (for UI layout)
|
||||
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<void> => {
|
||||
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<any> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
let operations: Promise<any>[] = [];
|
||||
for (const table of tableList) {
|
||||
await tx.update(tables).set({
|
||||
operations.push(tx.update(tables).set({
|
||||
posX: (table as any).posX,
|
||||
posY: (table as any).posY
|
||||
}).where(eq(tables.id, (table as any).id))
|
||||
}).where(eq(tables.id, (table as any).id)))
|
||||
}
|
||||
return await Promise.all(operations);
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
// Apply a list of diff operations to sync database
|
||||
const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => {
|
||||
const executeDbDiffOps = useCallback(async (diffOperations: DBDiffOperation[]) => {
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const operation of operations) {
|
||||
let operations: Promise<any>[] = [];
|
||||
|
||||
for (const operation of diffOperations) {
|
||||
|
||||
if (operation.type == "RENAME_DATABASE") {
|
||||
currentDatabaseId && await tx.update(databaseModel).set({
|
||||
name: operation.chnages.name
|
||||
}).where(eq(databaseModel.id, currentDatabaseId));
|
||||
if (currentDatabaseId)
|
||||
operations.push(tx.update(databaseModel).set({
|
||||
name: operation.chnages.name
|
||||
}).where(eq(databaseModel.id, currentDatabaseId)));
|
||||
}
|
||||
else if (operation.type == "UPDATE_NUM_TABLES") {
|
||||
console.log("update number of tables with ", operation.value);
|
||||
if (currentDatabaseId)
|
||||
operations.push(
|
||||
tx.update(databaseModel).set({
|
||||
numOfTables: operation.value
|
||||
}).where(eq(databaseModel.id, currentDatabaseId))
|
||||
)
|
||||
}
|
||||
|
||||
else if (operation.type == "CREATE_TABLE") {
|
||||
await tx.insert(tables).values(operation.table);
|
||||
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
|
||||
if (operation.table.fields && Object.values(operation.table.fields).length > 0)
|
||||
await tx.insert(fields).values(Object.values(operation.table.fields));
|
||||
operations.push(tx.insert(tables).values(operation.table));
|
||||
|
||||
if (operation.table.fields && Object.values(operation.table.fields).length > 0) {
|
||||
operations.push(
|
||||
tx.insert(fields).values(Object.values(operation.table.fields))
|
||||
);
|
||||
}
|
||||
|
||||
} else if (operation.type === "UPDATE_TABLE") {
|
||||
await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId));
|
||||
operations.push(
|
||||
tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId))
|
||||
)
|
||||
|
||||
} else if (operation.type === "DELETE_TABLE") {
|
||||
operations.push(
|
||||
deleteTablesWithCascade([operation.tableId], tx)
|
||||
);
|
||||
|
||||
await tx.delete(tables).where(eq(tables.id, operation.tableId));
|
||||
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
|
||||
|
||||
} else if (operation.type === "CREATE_FIELD") {
|
||||
await tx.insert(fields).values(operation.field);
|
||||
operations.push(
|
||||
tx.insert(fields).values(operation.field)
|
||||
);
|
||||
|
||||
} else if (operation.type === "DELETE_FIELD") {
|
||||
await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, operation.fieldId), eq(relationships.targetFieldId, operation.fieldId)));
|
||||
await tx.delete(fields).where(eq(fields.id, operation.fieldId));
|
||||
operations.push(
|
||||
deleteFieldsWithCascade([operation.fieldId], tx)
|
||||
)
|
||||
|
||||
} else if (operation.type === "UPDATE_FIELD") {
|
||||
await tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId));
|
||||
operations.push(
|
||||
tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId))
|
||||
);
|
||||
|
||||
} else if (operation.type === "CREATE_RELATIONSHIP") {
|
||||
await tx.insert(relationships).values(operation.relationship);
|
||||
operations.push(
|
||||
tx.insert(relationships).values(operation.relationship)
|
||||
);
|
||||
|
||||
} else if (operation.type === "DELETE_RELATIONSHIP") {
|
||||
await tx.delete(relationships).where(eq(relationships.id, operation.relationshipId));
|
||||
operations.push(
|
||||
tx.delete(relationships).where(eq(relationships.id, operation.relationshipId))
|
||||
);
|
||||
|
||||
} else if (operation.type === "UPDATE_RELATIONSHIP") {
|
||||
await tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId));
|
||||
operations.push(
|
||||
tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId))
|
||||
);
|
||||
}
|
||||
else if (operation.type == "CREATE_INDEX") {
|
||||
await tx.insert(indices).values(operation.index);
|
||||
operations.push(
|
||||
tx.insert(indices).values(operation.index)
|
||||
);
|
||||
if (operation.index.fieldIndices && Object.values(operation.index.fieldIndices).length > 0)
|
||||
await tx.insert(field_indices).values(Object.values(operation.index.fieldIndices));
|
||||
|
||||
operations.push(
|
||||
tx.insert(field_indices).values(Object.values(operation.index.fieldIndices))
|
||||
);
|
||||
}
|
||||
else if (operation.type == "DELETE_INDEX") {
|
||||
await tx.delete(indices).where(eq(indices.id, operation.indexId));
|
||||
operations.push(
|
||||
tx.delete(indices).where(eq(indices.id, operation.indexId))
|
||||
);
|
||||
}
|
||||
else if (operation.type == "UPDATE_INDEX") {
|
||||
await tx.update(indices).set(operation.changes).where(eq(indices.id, operation.indexId));
|
||||
operations.push(
|
||||
tx.update(indices).set(operation.changes).where(eq(indices.id, operation.indexId))
|
||||
);
|
||||
|
||||
} else if (operation.type == "UPDATE_FIELD_INDICES") {
|
||||
if (operation.delete.length > 0) {
|
||||
await tx.delete(field_indices).where(inArray(field_indices.id, operation.delete))
|
||||
operations.push(
|
||||
tx.delete(field_indices).where(inArray(field_indices.id, operation.delete))
|
||||
)
|
||||
}
|
||||
if (operation.create.length > 0) {
|
||||
await tx.insert(field_indices).values(operation.create);
|
||||
operations.push(
|
||||
tx.insert(field_indices).values(operation.create)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(operations);
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error
|
||||
}
|
||||
}, [db, currentDatabaseId]);
|
||||
@@ -361,45 +411,49 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
|
||||
if (currentDatabaseId) {
|
||||
return await db.transaction(async (tx) => {
|
||||
let operations: Promise<any>[] = [];
|
||||
for (const table of importedTables) {
|
||||
await tx.insert(tables).values({
|
||||
|
||||
operations.push(tx.insert(tables).values({
|
||||
...table,
|
||||
databaseId: currentDatabaseId,
|
||||
createdAt: table.createdAt || getTimestamp()
|
||||
} as TableInsertType);
|
||||
await updateDbNumTables(currentDatabaseId, tx);
|
||||
} as TableInsertType));
|
||||
|
||||
if (table.fields) {
|
||||
await tx.insert(fields).values(
|
||||
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
|
||||
operations.push(
|
||||
tx.insert(fields).values(
|
||||
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const relationship of importedRelationships) {
|
||||
await tx.insert(relationships).values({
|
||||
operations.push(tx.insert(relationships).values({
|
||||
...relationship,
|
||||
databaseId: currentDatabaseId,
|
||||
createdAt: relationship.createdAt || getTimestamp()
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
for (const index of importedIndices) {
|
||||
await tx.insert(indices).values({
|
||||
operations.push(tx.insert(indices).values({
|
||||
...index,
|
||||
createdAt: index.createdAt ? index.createdAt : getTimestamp()
|
||||
});
|
||||
|
||||
}));
|
||||
if (index.fieldIndices) {
|
||||
await tx.insert(field_indices).values(
|
||||
operations.push(tx.insert(field_indices).values(
|
||||
index.fieldIndices.map((fieldIndex: FieldIndexInsertType) => ({ ...fieldIndex, indexId: index.id }))
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
await Promise.all(operations);
|
||||
await updateDbNumTables(currentDatabaseId, tx)
|
||||
});
|
||||
} else {
|
||||
throw Error("no database selected")
|
||||
}
|
||||
}, [currentDatabaseId])
|
||||
}, [db, currentDatabaseId])
|
||||
|
||||
const databaseOpsValue = useMemo(() => ({
|
||||
isSwitchingDatabase,
|
||||
@@ -457,11 +511,8 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
getInteger,
|
||||
grouped_data_types
|
||||
]);
|
||||
|
||||
|
||||
return (
|
||||
<DatabaseDataContext.Provider value={{
|
||||
|
||||
database: database as unknown as DatabaseType,
|
||||
currentDatabaseId,
|
||||
databases: databases as DatabaseType[],
|
||||
@@ -471,23 +522,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
getField,
|
||||
}}>
|
||||
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
|
||||
{
|
||||
/* !database && !isLoading &&
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
{
|
||||
database && !isLoading && !isSwitchingDatabase &&
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>*/
|
||||
}
|
||||
{
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>
|
||||
}
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>
|
||||
</DatabaseOperationsContext.Provider>
|
||||
</DatabaseDataContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
import { createContext, Dispatch } from "react";
|
||||
|
||||
|
||||
@@ -10,7 +11,12 @@ interface DiagramOpsContextType {
|
||||
focusOnTable: (id: string, transition?: boolean) => void,
|
||||
focusOnRelationship: (id: string, transition?: boolean , withNavigate? : boolean) => void,
|
||||
setIsConnectionInProgress: Dispatch<boolean>,
|
||||
isConnectionInProgress: boolean
|
||||
isConnectionInProgress: boolean ,
|
||||
showController : boolean ,
|
||||
setShowController : ( value : boolean ) => void ,
|
||||
cardinalityStyle : CardinalityStyle ,
|
||||
setCardinalityStyle : (style : CardinalityStyle) => void
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useReactFlow } from "@xyflow/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { DiagramDataContext, DiagramOpsContext } from "./diagram-context";
|
||||
import { CardinalityStyle } from "@/lib/database";
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +17,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
const [focusedTableId, setFocusedTableId] = useState<string | undefined>(undefined)
|
||||
const [focusedRelationshipId, setFocusedRelationshipId] = useState<string | undefined>(undefined)
|
||||
const [isConnectionInProgress, setIsConnectionInProgress] = useState<boolean>(false);
|
||||
const [showController, setShowController] = useState<boolean>(true);
|
||||
const [cardinalityStyle, setCardinalityStyle] = useState<CardinalityStyle>(CardinalityStyle.SYMBOLIC);
|
||||
|
||||
const focusOnTable = useCallback((id: string, transition: boolean = false) => {
|
||||
navigate("/database/tables");
|
||||
@@ -23,6 +26,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
const selected: boolean = node.id === id;
|
||||
|
||||
|
||||
if (selected && transition) {
|
||||
fitView({
|
||||
duration: 500,
|
||||
@@ -33,6 +38,10 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
if (node.selected == selected)
|
||||
return node;
|
||||
|
||||
return {
|
||||
...node,
|
||||
selected,
|
||||
@@ -46,11 +55,12 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
const focusOnRelationship = useCallback((id: string, transition: boolean = false, withNavigate: boolean = true) => {
|
||||
if (withNavigate)
|
||||
navigate("/database/relationships");
|
||||
|
||||
|
||||
setFocusedRelationshipId(id);
|
||||
|
||||
setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
|
||||
const selected: boolean = edge.id === id;
|
||||
|
||||
if (selected && transition) {
|
||||
@@ -65,6 +75,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
}]
|
||||
});
|
||||
}
|
||||
if (edge.selected == selected)
|
||||
return edge
|
||||
return {
|
||||
...edge,
|
||||
selected
|
||||
@@ -86,8 +98,13 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
|
||||
focusOnRelationship,
|
||||
setIsConnectionInProgress,
|
||||
isConnectionInProgress,
|
||||
showController,
|
||||
setShowController,
|
||||
cardinalityStyle,
|
||||
setCardinalityStyle
|
||||
|
||||
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress])
|
||||
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle,
|
||||
setCardinalityStyle])
|
||||
return (
|
||||
<DiagramDataContext.Provider
|
||||
value={contextDatatValue}
|
||||
|
||||
@@ -6,7 +6,8 @@ export enum Modals {
|
||||
CREATE_DATABASE = "CREATE_DATABASE" ,
|
||||
OPEN_DATABASE = "OPEN_DATABASE" ,
|
||||
DELETE_DATABASE = "DELETE_DATABASE" ,
|
||||
IMPORT_DATABASE = "IMPOR_DATABASE"
|
||||
IMPORT_DATABASE = "IMPOR_DATABASE" ,
|
||||
EXPORT_SQL = "EXPORT_SQL"
|
||||
}
|
||||
|
||||
interface ModalContextType {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { CreateDatabaseModal } from "@/pages/database/modals/create-database-mod
|
||||
import OpenDatabaseModal from "@/pages/database/modals/open-database-modal";
|
||||
import DeleteDatabaseModal from "@/pages/database/modals/delete-database-modal";
|
||||
import ImportDatabaseModal from "@/pages/database/modals/import-database";
|
||||
import ExportSqlModal from "@/pages/database/modals/export-sql-modal";
|
||||
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
@@ -57,7 +58,10 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
|
||||
<DeleteDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
|
||||
||
|
||||
(currentModal.modal == Modals.IMPORT_DATABASE ) &&
|
||||
<ImportDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
|
||||
<ImportDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
|
||||
||
|
||||
(currentModal.modal == Modals.EXPORT_SQL ) &&
|
||||
<ExportSqlModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
|
||||
) : undefined
|
||||
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import { PowerSyncSQLiteDatabase, wrapPowerSyncWithDrizzle } from '@powersync/dr
|
||||
|
||||
export const powerSyncDb = new PowerSyncDatabase({
|
||||
database: {
|
||||
dbFilename: 'stackrender.sqlite'
|
||||
dbFilename: 'stackrender.sqlite',
|
||||
},
|
||||
schema: AppSchema,
|
||||
|
||||
});
|
||||
|
||||
export const db: PowerSyncSQLiteDatabase<typeof drizzleSchema> = wrapPowerSyncWithDrizzle(powerSyncDb, {
|
||||
schema: drizzleSchema
|
||||
schema: drizzleSchema,
|
||||
});
|
||||
|
||||
const ConnectorContext = createContext<StackRenderConnector | null>(null);
|
||||
@@ -33,17 +33,13 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
|
||||
const [connector] = useState(new StackRenderConnector());
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
powerSync.init();
|
||||
powerSync.connect(connector);
|
||||
|
||||
(async () => {
|
||||
const setup = async () => {
|
||||
await powerSync.init();
|
||||
await powerSync.execute("PRAGMA foreign_keys = ON;");
|
||||
})();
|
||||
powerSync.connect(connector);
|
||||
};
|
||||
setup();
|
||||
}, [powerSync, connector])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<PowerSyncContext.Provider value={powerSync}>
|
||||
|
||||
@@ -85,6 +85,7 @@ div[data-slot="content"] hr[role="separator"] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Target the scrollbar */
|
||||
@@ -131,12 +132,7 @@ div[data-slot="content"] hr[role="separator"] {
|
||||
|
||||
|
||||
|
||||
|
||||
.database-checkbox[data-selected="true"] {
|
||||
border: 1.5px solid hsl(var(--heroui-primary-300));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
thead[role="rowgroup"] th {
|
||||
border-radius: 0px !important;
|
||||
@@ -172,6 +168,7 @@ tbody[role="rowgroup"] tr td:first-child {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.cm-editor {
|
||||
|
||||
min-width: 100% !important;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { fields } from "@/lib/schemas/field-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
|
||||
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
|
||||
import { tables } from "@/lib/schemas/table-schema";
|
||||
import { relationships } from "@/lib/schemas/relationship-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { indices } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
|
||||
export const deleteFieldsWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
return Promise.all([
|
||||
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, ids), inArray(relationships.targetFieldId, ids))),
|
||||
tx.delete(field_indices).where(inArray(field_indices.fieldId, ids)),
|
||||
tx.delete(fields).where(inArray(fields.id, ids)),
|
||||
]);
|
||||
}
|
||||
|
||||
export const deleteTablesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
|
||||
let fieldIds: { id: string }[] | string[] = await tx.select({
|
||||
id: fields.id
|
||||
}).from(fields).where(inArray(fields.tableId, ids));
|
||||
|
||||
let indicesIds: { id: string }[] | string[] = await tx.select({
|
||||
id: indices.id
|
||||
}).from(indices).where(inArray(indices.tableId, ids));
|
||||
|
||||
fieldIds = fieldIds.map((fieldId: { id: string }) => fieldId.id);
|
||||
indicesIds = indicesIds.map((indexId: { id: string }) => indexId.id);
|
||||
|
||||
return Promise.all([
|
||||
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, fieldIds), inArray(relationships.targetFieldId, fieldIds))),
|
||||
tx.delete(field_indices).where(inArray(field_indices.fieldId, fieldIds)),
|
||||
tx.delete(fields).where(inArray(fields.id, fieldIds)),
|
||||
tx.delete(field_indices).where(inArray(field_indices.indexId, indicesIds)),
|
||||
tx.delete(indices).where(inArray(indices.id, indicesIds)),
|
||||
tx.delete(tables).where(inArray(tables.id, ids)),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
export const deleteIndicesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
|
||||
return Promise.all([
|
||||
tx.delete(field_indices).where(inArray(field_indices.indexId, ids)),
|
||||
tx.delete(indices).where(inArray(indices.id, ids))
|
||||
])
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export type DBDiffOperation =
|
||||
| { type: 'DELETE_INDEX'; tableId: string; indexId: string }
|
||||
| { type: 'UPDATE_INDEX'; tableId: string; indexId: string; changes: Partial<IndexType> }
|
||||
| { type: 'UPDATE_FIELD_INDICES'; tableId: string; indexId: string; create: FieldIndexType[]; delete: string[] }
|
||||
|
||||
| { type: "UPDATE_NUM_TABLES", value: number };
|
||||
|
||||
/**
|
||||
* Convert a list of low-level JSON patch operations into higher-level database diff operations
|
||||
@@ -58,6 +58,8 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
operations.push({ type: "RENAME_DATABASE", chnages: { name: op.value } as DatabaseInsertType })
|
||||
}
|
||||
|
||||
if (op.op == "replace" && parts[0] == "numOfTables")
|
||||
operations.push({ type: "UPDATE_NUM_TABLES", value: op.value })
|
||||
// Handle table-related changes
|
||||
else if (parts[0] == "tables") {
|
||||
const tableId = parts[1];
|
||||
@@ -209,8 +211,6 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Index creates
|
||||
for (const [tableId, indices] of Object.entries(indexCreates)) {
|
||||
for (const index of indices) {
|
||||
@@ -303,3 +303,6 @@ export function normalizeDatabase(db: DatabaseType): any {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+7
-1
@@ -1,4 +1,9 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { relationships } from "@/lib/schemas/relationship-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { eq, ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
|
||||
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
@@ -17,6 +22,7 @@ export const getNextSequence = (fields: FieldType[]): number => {
|
||||
|
||||
|
||||
|
||||
|
||||
export const cloneField = (field: FieldType): FieldType => {
|
||||
return {
|
||||
...field,
|
||||
|
||||
@@ -15,7 +15,8 @@ import { DatabaseInsertType } from "@/lib/schemas/database-schema";
|
||||
|
||||
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
|
||||
const parser = new Parser();
|
||||
|
||||
let errors : Error[] = [] ;
|
||||
|
||||
const createTableStatements: string[] = [];
|
||||
const alterTableStatements: string[] = [];
|
||||
const createIndexStatements: string[] = [];
|
||||
@@ -61,8 +62,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
postgresTypes.push(instructionAst[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
} catch (error ) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +86,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
|
||||
errors.push(error as Error);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -96,7 +98,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
indices.push(postgresAstToIndex(instructionAst[0], tables));
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
for (const alterTable of alterTableStatements) {
|
||||
@@ -107,6 +110,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
relationships = relationships.concat(extractedRelationships);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
@@ -129,13 +134,10 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
console.log (instructionAst)
|
||||
}
|
||||
if (instructionAst) {
|
||||
const table: TableInsertType = astToTable(instructionAst, data_types);
|
||||
|
||||
tables.push(table);
|
||||
|
||||
const tableForeignKeyConstraints = (instructionAst as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
|
||||
const tableReferenceDefinitions = (instructionAst as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
|
||||
|
||||
@@ -153,16 +155,16 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -170,7 +172,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -190,6 +192,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
|
||||
@@ -208,13 +211,17 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
indices.push(astToIndex(instructionAst, tables));
|
||||
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( tables.length == 0 )
|
||||
throw Error ("Error while Parsing")
|
||||
|
||||
|
||||
return { tables, relationships, indices };
|
||||
return { tables, relationships, indices , errors };
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
}
|
||||
|
||||
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
|
||||
|
||||
|
||||
const transaction = await database.getNextCrudTransaction();
|
||||
|
||||
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +64,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let batch: any[] = [];
|
||||
for (let operation of transaction.crud) {
|
||||
|
||||
@@ -102,7 +103,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
|
||||
|
||||
localStorage.setItem("last_upload_at", new Date().toISOString());
|
||||
window.dispatchEvent(new StorageEvent("storage", { key: "lastUploadAt" }));
|
||||
|
||||
|
||||
} catch (ex: any) {
|
||||
console.debug(ex);
|
||||
throw ex;
|
||||
|
||||
+7
-2
@@ -9,6 +9,7 @@ import { v4 } from "uuid";
|
||||
import { getTimestamp } from "./utils";
|
||||
import { SortableTable } from "@/lib/table";
|
||||
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
const adjustTablesPositions = async (
|
||||
@@ -27,12 +28,12 @@ const adjustTablesPositions = async (
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
|
||||
'elk.spacing.nodeNode': '80',
|
||||
|
||||
|
||||
},
|
||||
children: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
width: node.measured?.width ?? 224,
|
||||
height: node.measured?.height ?? 150,
|
||||
height: node.measured?.height ?? ((node.data?.table as TableType)?.fields?.length * 32 + 36),
|
||||
})),
|
||||
edges: relationships.map((rel) => ({
|
||||
id: `${rel.sourceTableId}->${rel.targetTableId}`,
|
||||
@@ -195,6 +196,10 @@ function orderTables(tables: SortableTable[]): string[] {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export {
|
||||
adjustTablesPositions,
|
||||
getDefaultTableOverlapping,
|
||||
|
||||
+8
-2
@@ -1,3 +1,5 @@
|
||||
import { drizzleSchema } from "@/lib/schemas/app-schema";
|
||||
|
||||
const areArraysEqual = (a: string[], b: string[]): boolean => {
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
@@ -64,11 +66,15 @@ function groupBy(array: any[], key: string) {
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export {
|
||||
areArraysEqual,
|
||||
getTimestamp,
|
||||
excludeFields,
|
||||
groupBy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+52
-6
@@ -6,8 +6,8 @@ export default {
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
|
||||
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
|
||||
|
||||
],
|
||||
darkMode: "class",
|
||||
|
||||
@@ -32,8 +32,31 @@ export default {
|
||||
100: "#1c2026"
|
||||
},
|
||||
|
||||
danger: {
|
||||
DEFAULT: "#F92814"
|
||||
danger: {
|
||||
DEFAULT: "#F92814",
|
||||
50: '#fff1f0',
|
||||
100: '#ffe0dd',
|
||||
200: '#fcb8b1',
|
||||
300: '#f78c81',
|
||||
400: '#f46156',
|
||||
500: '#F92814', // base
|
||||
600: '#d01711',
|
||||
700: '#a2120d',
|
||||
800: '#760d09',
|
||||
900: '#4a0805'
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "#ffb400",
|
||||
50: '#fffbea',
|
||||
100: '#fff3c4',
|
||||
200: '#ffe88a',
|
||||
300: '#ffdc4f',
|
||||
400: '#ffd01b',
|
||||
500: '#ffb400', // base
|
||||
600: '#cc9000',
|
||||
700: '#a17100',
|
||||
800: '#755200',
|
||||
900: '#4a3400'
|
||||
},
|
||||
font: {
|
||||
DEFAULT: "#cecfd2"
|
||||
@@ -41,7 +64,7 @@ export default {
|
||||
content1: {
|
||||
DEFAULT: "#20252c"
|
||||
},
|
||||
|
||||
|
||||
default: {
|
||||
50: '#f2f3f5',
|
||||
100: '#d6d7db',
|
||||
@@ -88,7 +111,30 @@ export default {
|
||||
|
||||
colors: {
|
||||
danger: {
|
||||
DEFAULT: "#F92814"
|
||||
DEFAULT: "#F92814",
|
||||
50: '#fff1f0',
|
||||
100: '#ffe0dd',
|
||||
200: '#fcb8b1',
|
||||
300: '#f78c81',
|
||||
400: '#f46156',
|
||||
500: '#F92814', // base
|
||||
600: '#d01711',
|
||||
700: '#a2120d',
|
||||
800: '#760d09',
|
||||
900: '#4a0805'
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "#ffb400",
|
||||
50: '#fffbea',
|
||||
100: '#fff3c4',
|
||||
200: '#ffe88a',
|
||||
300: '#ffdc4f',
|
||||
400: '#ffd01b',
|
||||
500: '#ffb400', // base
|
||||
600: '#cc9000',
|
||||
700: '#a17100',
|
||||
800: '#755200',
|
||||
900: '#4a3400'
|
||||
},
|
||||
font: {
|
||||
DEFAULT: "#333639"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// vite.config.ts
|
||||
import { defineConfig } from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite/dist/node/index.js";
|
||||
import react from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/@vitejs/plugin-react/dist/index.mjs";
|
||||
import tsconfigPaths from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-tsconfig-paths/dist/index.mjs";
|
||||
import topLevelAwait from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-plugin-top-level-await/exports/import.mjs";
|
||||
var vite_config_default = defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), topLevelAwait()],
|
||||
server: {
|
||||
port: 3e3
|
||||
},
|
||||
optimizeDeps: {
|
||||
// Don't optimize these packages as they contain web workers and WASM files.
|
||||
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
|
||||
exclude: ["@journeyapps/wa-sqlite", "@powersync/web"],
|
||||
include: ["@powersync/web > js-logger"]
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: () => [topLevelAwait()]
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90YW1hbi9PbmVEcml2ZS9EZXNrdG9wL3N0YWNrcmVuZGVyL3N0YWNrcmVuZGVyL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCdcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnXG5pbXBvcnQgdG9wTGV2ZWxBd2FpdCBmcm9tICd2aXRlLXBsdWdpbi10b3AtbGV2ZWwtYXdhaXQnO1xuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpLCB0c2NvbmZpZ1BhdGhzKCkgLCB0b3BMZXZlbEF3YWl0KCldLFxuICBzZXJ2ZXIgOiB7IFxuICAgIHBvcnQgOiAzMDAwXG4gIH0gLCBcbiAgb3B0aW1pemVEZXBzOiB7XG4gICAgLy8gRG9uJ3Qgb3B0aW1pemUgdGhlc2UgcGFja2FnZXMgYXMgdGhleSBjb250YWluIHdlYiB3b3JrZXJzIGFuZCBXQVNNIGZpbGVzLlxuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS92aXRlanMvdml0ZS9pc3N1ZXMvMTE2NzIjaXNzdWVjb21tZW50LTE0MTU4MjA2NzNcbiAgICBleGNsdWRlOiBbJ0Bqb3VybmV5YXBwcy93YS1zcWxpdGUnLCAnQHBvd2Vyc3luYy93ZWInXSxcbiAgICBpbmNsdWRlOiBbJ0Bwb3dlcnN5bmMvd2ViID4ganMtbG9nZ2VyJ11cbiAgfSxcbiAgd29ya2VyOiB7XG4gICAgZm9ybWF0OiAnZXMnLFxuICAgIHBsdWdpbnM6ICgpID0+IFsgdG9wTGV2ZWxBd2FpdCgpXVxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxVyxTQUFTLG9CQUFvQjtBQUNsWSxPQUFPLFdBQVc7QUFDbEIsT0FBTyxtQkFBbUI7QUFDMUIsT0FBTyxtQkFBbUI7QUFFMUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sR0FBRyxjQUFjLEdBQUksY0FBYyxDQUFDO0FBQUEsRUFDcEQsUUFBUztBQUFBLElBQ1AsTUFBTztBQUFBLEVBQ1Q7QUFBQSxFQUNBLGNBQWM7QUFBQTtBQUFBO0FBQUEsSUFHWixTQUFTLENBQUMsMEJBQTBCLGdCQUFnQjtBQUFBLElBQ3BELFNBQVMsQ0FBQyw0QkFBNEI7QUFBQSxFQUN4QztBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sUUFBUTtBQUFBLElBQ1IsU0FBUyxNQUFNLENBQUUsY0FBYyxDQUFDO0FBQUEsRUFDbEM7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
||||
Reference in New Issue
Block a user