UX improvements + Manage loading states + Numerical Cardinalities implemented

This commit is contained in:
KarimTamani
2025-06-30 17:40:45 +01:00
parent 3ddb1f4e39
commit 815845e1e9
56 changed files with 1309 additions and 790 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+6 -4
View File
@@ -23,11 +23,13 @@ function App() {
<ReactFlowProvider>
<DatabaseProvider>
<DiagramProvider>
<ModalProvider>
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}>
<ModalProvider>
{appRoutes}
</TooltipProvider>
</ModalProvider>
</ModalProvider>
</TooltipProvider>
</DiagramProvider>
</DatabaseProvider>
</ReactFlowProvider>
+23 -16
View File
@@ -1,6 +1,6 @@
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react";
import { Key } from "react";
import { Key, useState } from "react";
interface AutocompleteProps {
@@ -16,12 +16,13 @@ interface AutocompleteProps {
const headingClasses =
"flex w-full py-1.5 px-2 bg-default shadow-small rounded-small";
"flex w-full py-1.5 px-2 bg-default shadow-md border-1 font-medium rounded-md dark:border-font/5 dark:bg-background-100";
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => {
const onItemChange = (item: Key | null) => {
onSelectionChange && onSelectionChange(item);
}
@@ -30,7 +31,7 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
<HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
className="w-full"
defaultItems={items || []}
size="sm"
onSelectionChange={onItemChange}
@@ -39,11 +40,15 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
aria-label={placeholder}
placeholder={placeholder}
selectedKey={selectedItem as any}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
clearButton: "text-icon",
selectorButton: "text-icon",
popoverContent: "rounded-md "
}}
inputProps={{
classNames: {
inputWrapper: "border-divider group-hover:border-primary",
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
},
}}
@@ -55,35 +60,37 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
return <HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
className="w-full"
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
className="h-8 w-full focus-visible:ring-0 shadow-none "
selectedKey={selectedItem as any}
classNames={{
clearButton: "text-icon",
selectorButton: "text-icon",
popoverContent: "rounded-md "
}}
inputProps={{
classNames: {
inputWrapper: "border-divider group-hover:border-primary",
inputWrapper: "border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
},
}}
>
{
items ? Object.keys(items).map((key: any) => (
items ? Object.keys(items).map((key: string) => (
<AutocompleteSection
classNames={{
classNames={{
heading: headingClasses,
}}
title={key}
title={key.toUpperCase()}
>
{
items[key].map((item: any) => (
items[key as any].map((item: any) => (
<AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>
))
}
@@ -1,17 +1,22 @@
import { cn } from "@heroui/react";
import React from "react";
export interface CardinalityMarkerProps {
selected?: boolean,
direction?: "start" | "end",
type: "one" | "many"
cardinality: "one" | "many",
type?: "symbole" | "numeric"
}
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, type, direction = "start" }) => {
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", type = "symbole" }) => {
const id = `${type}_${direction}${selected ? "_selected" : ""}`;
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
const renderMarker = () => {
if (type == "many") {
if (cardinality == "many") {
if (direction == "start")
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
else if (direction == "end")
@@ -19,7 +24,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
}
if (type == "one") {
if (cardinality == "one") {
if (direction == "start") {
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
}
@@ -27,34 +32,78 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
}
}
}
return (
<>
if (type == "symbole")
return (
<>
<marker
id={id}
markerWidth="24"
markerHeight="24"
refX="12"
refY="12"
orient="auto"
markerUnits="userSpaceOnUse"
>
<svg
fill="transparent"
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
strokeWidth="4"
width="24"
height="24"
viewBox="0 0 100 100">
{
renderMarker()
}
</svg>
</marker>
</>
)
else if (type == "numeric") {
return (
<marker
id={id}
viewBox="0 0 24 24"
markerWidth="24"
markerHeight="24"
refX="12"
refX={direction == "start" ? "4" : "20"}
refY="12"
orient="auto"
markerUnits="userSpaceOnUse"
>
<svg
fill="transparent"
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
strokeWidth="4"
width="24"
height="24"
viewBox="0 0 100 100">
{
renderMarker()
<circle
cx="12"
cy="12"
r="6"
stroke-width="1"
className={
cn("dark:fill-default fill-default",
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
)
}
</svg>
/>
<text
x="12"
y="13"
text-anchor="middle"
dominant-baseline="middle"
font-size="7"
className={
cn("fill-font/90 dark:fill-font/90" ,
selected ? "fill-primary dark:fill-primary" : "" ,
)
}
>
{cardinality == "one" ? "I" : "N"}
</text>
</marker>
</>
)
)
}
}
export default CardinalityMarker;
export default React.memo( CardinalityMarker);
@@ -17,9 +17,11 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
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 " ,
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 "
}}
>
<div className="min-w-full h-full flex items-center justify-center ">
<Image
+22 -15
View File
@@ -1,40 +1,47 @@
import { ImportDatabaseMethod, ImportDatabaseOption } from "@/lib/database"
import { Avatar, Checkbox, Chip } from "@heroui/react";
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
interface OptionCheckboxProps {
value: string;
label: string ;
label: string;
icon?: React.ReactNode;
logo?: string
logo?: string;
isSelected?: boolean;
}
const OptionCheckbox: React.FC<OptionCheckboxProps> = ({ value, icon, logo , label }) => {
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
const { value, icon, logo, label, isSelected } = props;
const variant = isSelected ? {
variant: "flat",
color: "primary"
} : {
variant: "bordered",
color: "default"
}
return (
<Checkbox
aria-label={value}
value={value}
size="sm"
size="sm"
className="option-checkbox"
classNames={{
wrapper: "hidden",
label: "flex items-center justify-center w-full h-full"
label: "flex items-center justify-center w-full h-full "
}}
>
<Chip radius="sm" variant="bordered" color="default" className="option-span px-2 border-1 transition-all duration-300 border-divider"
avatar={logo ? <Avatar src={logo} /> : undefined}
<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
)}
avatar={logo ? <img src={logo} /> : undefined}
startContent={
icon
}
>
<span className="text-sm text-font font-semibold">
<span className="text-sm font-medium">
{label}
</span>
</Chip>
+60
View File
@@ -0,0 +1,60 @@
import React, { useCallback, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { useTranslation } from "react-i18next";
import { Button } from "@heroui/react";
import { Copy, CopyCheck } from "lucide-react";
interface ClipboardProps {
text?: string
}
const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
const { t } = useTranslation();
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = useCallback(async () => {
if (text)
try {
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 500)
} catch (err) {
setIsCopied(false);
}
}, [text])
return (
<Tooltip>
<TooltipTrigger>
<span>
<Button
size="sm"
isIconOnly
variant="bordered"
className="text-font/90 border-divider"
onPressEnd={copyToClipboard}
>
{
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{!isCopied ? t("clipboard.copy") : t("clipboard.copied")}
</TooltipContent>
</Tooltip>
)
};
export default React.memo(Clipboard)
+2 -14
View File
@@ -7,26 +7,14 @@ import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
interface MenuProps {
}
const Menu: React.FC<MenuProps> = ({ }) => {
const Menu: React.FC<any > = ({ }) => {
const { setTheme } = useTheme()
const { t } = useTranslation();
const { open } = useModal();
useEffect(() => {
open(Modals.IMPORT_DATABASE)
} , [])
const menu: MenuDropdownProps[] = useMemo(() => [
{
title: t("menu.file"),
+22
View File
@@ -0,0 +1,22 @@
import { Spinner } from "@heroui/react";
interface LoadingProps {
text?: string;
}
export const Loading: React.FC<LoadingProps> = ({ text }) => {
return (
<div className="fixed bg-overlay/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[99] left-0 top-0 text-white flex-col gap-2">
<Spinner size="lg" />
<span className="font-medium">
Loading ...
</span>
</div>
)
}
+29 -11
View File
@@ -8,13 +8,14 @@ import {
ModalFooter,
Button,
useDraggable,
cn,
} from "@heroui/react";
import { useTranslation } from "react-i18next";
export interface ModalProps {
isOpen?: boolean,
onOpenChange? : (open: boolean) => void,
onOpenChange?: (open: boolean) => void,
className?: string,
backdrop?: "blur" | "transparent" | "opaque",
title: string,
@@ -23,10 +24,11 @@ export interface ModalProps {
actionHandler?: () => void,
isDisabled?: boolean,
header?: string,
variant?: "default" | "danger"
variant?: "default" | "danger",
closable?: boolean
}
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default" }) => {
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default", closable = true }) => {
const targetRef = React.useRef(null);
@@ -39,17 +41,21 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
setIsLoading(true)
actionHandler && await actionHandler();
setIsLoading(false);
onClose()
}
return (
<HeroUiModal
ref={targetRef}
isOpen={isOpen}
onOpenChange={onOpenChange ? onOpenChange : undefined}
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
className={className}
backdrop={backdrop}
radius="sm"
classNames={{
base: "dark:bg-background-100",
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md" , !closable ? "hidden" : "")
}}
>
<ModalContent>
{(onClose) => (
@@ -72,9 +78,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
{
variant == "default" &&
<>
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
{
closable &&
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
@@ -83,9 +95,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
{
variant == "danger" &&
<>
<Button color="default" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
{
closable &&
<Button color="default" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
+25 -13
View File
@@ -2,8 +2,11 @@ import { usePowerSync } from "@powersync/react";
import { Wifi, WifiOff } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { cn, Divider, Progress } from "@heroui/react";
import { addToast, cn, Divider, Progress } from "@heroui/react";
import { differenceInHours, differenceInMinutes } from "date-fns";
import { useTranslation } from "react-i18next";
const ConnectionStatus: React.FC = () => {
@@ -11,6 +14,10 @@ const ConnectionStatus: React.FC = () => {
const [syncStatus, setSyncStatus] = useState(powerSync.currentStatus);
const [downloadProgress, setDownloadProgress] = useState<number | undefined>(undefined);
const clearProgressHandler: any = useRef(undefined);
const { t } = useTranslation();
const [lastUploadAt, setLastUploadAt] = useState<Date | undefined>(() => {
const last_upload_at = localStorage.getItem("last_upload_at");
if (last_upload_at)
@@ -65,20 +72,23 @@ const ConnectionStatus: React.FC = () => {
const hours = differenceInHours(currentDate, lastUploadAt);
if (hours > 0 && hours < 24) {
return `${hours} hour ago`
return `${hours} ${t("connection_status.hour_ago")}`
}
if (minutes > 0 && minutes < 60)
return `${minutes} min ago`
return `${minutes} ${t("connection_status.min_ago")}`
if (hours > 24) {
return `${lastUploadAt.toLocaleDateString()}`
}
}
return "Just now";
}, [lastUploadAt]);
return t("connection_status.just_now") ;
}, [lastUploadAt , t]);
const lastSyncedDate: string | undefined = useMemo(() => {
return syncStatus.lastSyncedAt ? syncStatus.lastSyncedAt.toLocaleString("en-US") : undefined;
}, [syncStatus.lastSyncedAt]);
return (
<div className="flex items-center ">
<Tooltip>
@@ -89,10 +99,9 @@ const ConnectionStatus: React.FC = () => {
syncStatus.connected ?
<Wifi className="size-4 " /> :
<WifiOff className="size-4 " />
}
{
syncStatus.connected ? "Online" : "Offline"
syncStatus.connected ? t("connection_status.online") : t("connection_status.offline")
}
</span>
@@ -103,7 +112,7 @@ const ConnectionStatus: React.FC = () => {
downloadProgress !== undefined ?
<>
<span className="text-font/90 w-[96px] truncate">
Saving ...
{t("connection_status.saving")} ...
</span>
<Progress
@@ -122,16 +131,19 @@ const ConnectionStatus: React.FC = () => {
</>
:
<span className="text-font/70 truncate font-semibold" >
Saved {deltaUploadTime}
{t("connection_status.saved")} {deltaUploadTime}
</span>
}
</div>
</TooltipTrigger>
<TooltipContent className="dark:bg-default-900">
Last synced {syncStatus.lastSyncedAt?.toISOString()}
</TooltipContent>
{
lastSyncedDate &&
<TooltipContent >
{t("connection_status.last_synced")} : {lastSyncedDate}
</TooltipContent>
}
</Tooltip>
</div>
)
+10 -12
View File
@@ -42,13 +42,13 @@ const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
<Tooltip>
<TooltipTrigger asChild>
<label
className=" w-full text-editable truncate h-8 px-3 text-sm font-bold dark:text-white flex gap-4 items-center justify-center text-font/90 hover:underline"
className=" w-full text-editable truncate h-8 px-3 text-sm font-bold dark:text-white flex gap-4 items-center justify-center text-font hover:underline"
onDoubleClick={() => setEditMode(true)}
>
<Image
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
width={22}
className=" rounded-none "
src={getDatabaseByDialect(database.dialect as DatabaseDialect).small_logo}
width={20}
className="rounded-none "
/>
{dbName}
@@ -64,27 +64,25 @@ const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
editMode &&
<div className="flex justify-center items-center gap-4">
<Image
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
width={32}
src={getDatabaseByDialect(database.dialect as DatabaseDialect).small_logo}
width={30}
className=" rounded-none "
/>
<Input
placeholder={database.name}
type="text"
size="sm"
radius="sm"
variant="faded"
variant="bordered"
onChange={(event: any) => setDbName(event.target.value)}
value={dbName}
onBlur={saveDatabaseName}
autoFocus
className="h-8 w-full focus-visible:ring-0 shadow-none"
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
input: "font-semibold text-black dark:text-font"
}}
/>
<Button
variant="solid"
+3 -3
View File
@@ -48,9 +48,9 @@ const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange })
<ReactTagInput
tags={tags}
classNames={{
tag: "font-normal inline-block m-0.5 border-1 border-default rounded-full px-2 py-1 flex-row ",
remove: " bg-default-900 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-default-400 h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
tag: "font-normal inline-block m-0.5 border-1 border-default dark:border-divider rounded-full px-2 py-1 flex-row ",
remove: " bg-default-900 dark:bg-default-500 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-primary h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
tags : "max-h-[256px] overflow-auto "
}}
handleDelete={handleDelete}
-22
View File
@@ -1,22 +0,0 @@
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export const useRelationshipName = (relationship: RelationshipType) => {
const { t } = useTranslation();
const name: string = useMemo(() => {
return getDefaultRelationshipName(relationship) ;
}, [relationship, t])
return {
name
}
}
export const getDefaultRelationshipName = (relationship: RelationshipType) => {
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
return "";
return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk`
}
+5 -6
View File
@@ -3,13 +3,14 @@ import { TableType } from "@/lib/schemas/table-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { useEffect } from "react";
import { getDefaultTableOverlapping } from "@/utils/tables";
import hash from "object-hash";
import hash from "object-hash";
import { useDatabase } from "@/providers/database-provider/database-provider";
export const useTableToNode = (tables: TableType[]): void => {
const { setNodes } = useReactFlow();
const { setNodes } = useReactFlow();
useEffect(() => {
const tableNodes = tables.map((table: TableType) => {
@@ -31,20 +32,18 @@ export const useTableToNode = (tables: TableType[]): void => {
}
} 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])
+29 -1
View File
@@ -64,7 +64,7 @@ export const ar = {
}
},
table: {
double_click: "انقر مزدوجًا للتعديل" ,
double_click: "انقر مزدوجًا للتعديل",
overlapping_tables: "الجداول المتداخلة",
},
control_buttons: {
@@ -104,6 +104,34 @@ export const ar = {
help: "مساعدة",
show_docs: "عرض المستندات",
join_discord: "الانضمام إلى ديسكورد"
},
connection_status: {
online: "متصل",
offline: "غير متصل",
saving: "جارٍ الحفظ",
saved: "تم الحفظ",
last_synced: "آخر مزامنة"
},
import: {
instructions: "التعليمات",
install: "التثبيت",
run_command: "قم بتشغيل الأمر التالي في الطرفية.",
example: "مثال",
copy_code: "انسخ محتوى ملف .sql إلى قسم الكود أدناه.",
pg_admin: {
step1: "افتح <bold>Pg Admin</bold>.",
step2: "انقر بزر الفأرة الأيمن على قاعدة البيانات واختر <bold>Backup</bold> من القائمة السياقية.",
step3: "قم بتسمية ملف <code>.sql</code>، ثم اختر التنسيق <bold>Plain</bold>، وحدد <bold>Encoding: UTF8</bold>.",
step4: "تأكد من أن <bold>Only schema</bold> محددة، و<bold>Only data</bold> غير محددة، وذلك في علامة تبويب <bold>Data Options</bold>.",
step5: "انقر على <bold>Backup</bold> لتصدير الملف، ثم انسخ محتواه إلى قسم محرر الكود أدناه."
},
workbench: {
step1: "افتح <bold>MySQL Workbench</bold> و<bold>اتصل</bold> بخادم MySQL الخاص بك.",
step2: "من القائمة العلوية، انتقل إلى <bold>Server &gt; Data Export</bold>.",
step3: "في قسم <bold>خيارات التصدير</bold>، اختر <bold>Dump Structure Only</bold>.",
step4: "حدد <bold>Export to Self-Contained File</bold>، ثم اختر مكان الحفظ واسم ملف الإخراج <code>.sql</code>.",
step5: "انقر على <bold>Start Export</bold> لبدء عملية التصدير، ثم انسخ محتوى الملف إلى قسم محرر الكود أدناه."
}
}
}
};
+97 -43
View File
@@ -56,43 +56,43 @@ export const en = {
title: "Field Setting",
unique: "Unique",
unsigned: "Unsigned",
numeric_setting : "Numeric Setting" ,
decimal_setting : "Decimal Setting" ,
numeric_setting: "Numeric Setting",
decimal_setting: "Decimal Setting",
zeroFill: "Zero Fill",
autoIncrement: "Auto Increment",
note: "Note",
delete_field: "Delete Field",
field_note: "Field note",
precision : "Precision" ,
text_setting : "Text Setting" ,
charset : "Charset" ,
collation : "Collation" ,
scale : "Scale" ,
max_length : "Max length" ,
integer_width : "Integer Width" ,
width : "width" ,
default_value : "Default value" ,
value : "Value" ,
length : "Length" ,
values : "Values" ,
type_enter : "Type and press enter" ,
precision_def : "Total digits allowed (before + after the decimal)." ,
scale_def : "Digits allowed after the decimal." ,
time_default_value : {
no_value : "No value" ,
custom : "Custom time" ,
now : "Now"
} ,
errors : {
max_length : "must be positive number, no decimals." ,
integer_default_value : "Invalid default value for Integer" ,
precision : "Precision must be positive number, no decimals." ,
scale : "Scale must be positive number, no decimals." ,
scale_max_value : "Scale must be ≤ precision."
} ,
pick_value : "Pick value"
precision: "Precision",
text_setting: "Text Setting",
charset: "Charset",
collation: "Collation",
scale: "Scale",
max_length: "Max length",
integer_width: "Integer Width",
width: "width",
default_value: "Default value",
value: "Value",
length: "Length",
values: "Values",
type_enter: "Type and press enter",
precision_def: "Total digits allowed (before + after the decimal).",
scale_def: "Digits allowed after the decimal.",
time_default_value: {
no_value: "No value",
custom: "Custom time",
now: "Now"
},
errors: {
max_length: "must be positive number, no decimals.",
integer_default_value: "Invalid default value for Integer",
precision: "Precision must be positive number, no decimals.",
scale: "Scale must be positive number, no decimals.",
scale_max_value: "Scale must be ≤ precision."
},
pick_value: "Pick value"
},
delete: "Delete",
@@ -111,13 +111,13 @@ export const en = {
invalid_relationship: {
title: "Invalid Relationship",
description: "The source key type does not match the referenced key type. Please ensure both keys have the same data type."
} ,
circular_dependency : {
title : "Circular Dependency Detected" ,
toast_description : "A circular reference between tables was found. Check the diagram on the left and remove one of the relationships to fix it." ,
description : "Your schema contains a circular foreign key relationship between tables. To fix it" ,
suggestion : "remove one of the relationships listed below that are causing the cycle." ,
remove_relationship : "Remove relationship"
},
circular_dependency: {
title: "Circular Dependency Detected",
toast_description: "A circular reference between tables was found. Check the diagram on the left and remove one of the relationships to fix it.",
description: "Your schema contains a circular foreign key relationship between tables. To fix it",
suggestion: "remove one of the relationships listed below that are causing the cycle.",
remove_relationship: "Remove relationship"
}
},
table: {
@@ -180,13 +180,67 @@ export const en = {
open_database_header: "Open a database by selecting one from the list.",
delete_database: "Delete Database",
delete_database_content: "This action is irreversible and will permanently remove the diagram.",
delete: "Delete" ,
delete: "Delete",
import_database : {
title : "Import your Database" ,
import : "Import" ,
import_options : "Would you like to import using :"
import_database: {
title: "Import your Database",
import: "Import",
import_options: "Would you like to import using :"
}
},
clipboard: {
copy: "Copy",
copied: "Copied"
},
connection_status : {
online: "Online" ,
offline : "Offline" ,
saving : "Saving" ,
saved : "Saved" ,
last_synced : "Last synced" ,
min_ago : "min ago" ,
hour_ago : "hour ago" ,
just_now : "Just now"
},
import: {
instructions: "Instructions",
install: "Install",
run_command: "Run the following command in your terminal.",
example: "Example",
copy_code: "Copy the content of .sql file in code section below.",
pg_admin: {
"step1": "Open <bold>Pg Admin</bold>.",
"step2": "Right-click your database and select <bold>Backup</bold> from the context menu.",
"step3": "Name your <code>.sql</code> file, set Format to <bold>Plain</bold>, and choose <bold>Encoding: UTF8.</bold>",
"step4": "Make sure <bold>Only schema</bold> is checked and <bold>Only data</bold> is unchecked in the <bold>Data Options tab</bold>.",
"step5": "Click <bold>Backup</bold> to export the file, then copy its content into the code editor section below."
},
workbench: {
"step1": "Open <bold>MySQL Workbench</bold> and <bold>connect</bold> to your MySQL server.",
"step2": "In the top menu, go to <bold>Server > Data Export</bold>.",
"step3": "In the <bold>Export Options</bold>, choose <bold>Dump Structure Only</bold>.",
"step4": "Check <bold>Export to Self-Contained File</bold>, then choose a location and enter a name for the output <code>.sql</code> file.",
"step5": "Click <bold>Start Export</bold> to begin the export process. Then, copy its content into the code editor section below."
},
heidisql: {
"step1": "<bold>Open HeidiSQL</bold> and connect to your server.",
"step2": "In the <bold>left sidebar, right-click</bold> on the database you want to export.",
"step3": "Choose <bold>Export database as SQL</bold> from the context menu.",
"step4": "Select <bold>No data</bold> and make sure <bold>Create</bold> is checked to export the table structure only.",
"step5": "Click the <bold>Export</bold> button, and finally copy the content of the <code>.sql</code> file into the <bold>code editor</bold> below."
},
dbbrowser: {
step1: "Launch <bold>DB Browser for SQLite</bold>.",
step2: "Click <bold>File > Open Database</bold> and select your <code>.sqlite</code> or <code>.db</code> file.",
step3: "Go to <bold>File > Export > Database to SQL file</bold> from the top menu.",
step4: "In the dialog, choose <bold>Export schema only</bold> and click <bold>Save</bold>.",
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
},
}
}
+23 -1
View File
@@ -65,7 +65,7 @@ export const fr = {
}
},
table: {
double_click: "Double-cliquez pour éditer" ,
double_click: "Double-cliquez pour éditer",
overlapping_tables: "Tables qui se chevauchent",
},
control_buttons: {
@@ -105,6 +105,28 @@ export const fr = {
help: "Aide",
show_docs: "Afficher la documentation",
join_discord: "Rejoindre Discord"
},
import: {
instructions: "Instructions",
install: "Installer",
run_command: "Exécutez la commande suivante dans votre terminal.",
example: "Exemple",
copy_code: "Copiez le contenu du fichier .sql dans la section de code ci-dessous.",
pg_admin: {
step1: "Ouvrez <bold>Pg Admin</bold>.",
step2: "Faites un clic droit sur votre base de données et sélectionnez <bold>Sauvegarder</bold> dans le menu contextuel.",
step3: "Nommez votre fichier <code>.sql</code>, définissez le format sur <bold>Plain</bold> et choisissez <bold>Encodage : UTF8.</bold>",
step4: "Assurez-vous que <bold>Only schema</bold> est coché et que <bold>Only data</bold> ne l'est pas dans l'onglet <bold>Data Options</bold>.",
step5: "Cliquez sur <bold>Sauvegarder</bold> pour exporter le fichier, puis copiez son contenu dans la section de l'éditeur de code ci-dessous."
},
workbench: {
step1: "Ouvrez <bold>MySQL Workbench</bold> et <bold>connectez-vous</bold> à votre serveur MySQL.",
step2: "Dans le menu du haut, allez dans <bold>Server &gt; Data Export</bold>.",
step3: "Dans les <bold>Options d'exportation</bold>, choisissez <bold>Dump Structure Only</bold>.",
step4: "Cochez <bold>Export to Self-Contained File</bold>, puis choisissez un emplacement et donnez un nom au fichier <code>.sql</code> de sortie.",
step5: "Cliquez sur <bold>Démarrer l'exportation</bold> pour lancer le processus. Ensuite, copiez son contenu dans la section de l'éditeur de code ci-dessous."
}
}
}
};
+13 -13
View File
@@ -1,5 +1,4 @@
import { tags as t } from '@lezer/highlight';
import { EditorView } from '@uiw/react-codemirror';
export const colorOptions = [
@@ -26,29 +25,30 @@ export const randomColor = () => {
export const overrideDarkTheme = EditorView.theme({
'.cm-content': {
backgroundColor: "#20252c"
backgroundColor: "#20252c" ,
},
".cm-gutter": {
backgroundColor: "#20252c",
},
},
".cm-gutterElement": {
color: "#4b515a"
},
".ͼp": {
".ͼp": {
color: "#A994FF"
},
".cm-line .ͼq": {
color : "#ff6363"
} ,
".ͼu" : {
color : "#B6E672"
} ,
color: "#ff6363"
},
".ͼu": {
color: "#B6E672"
},
".ͼv": {
color : "#6cdcc4"
color: "#6cdcc4"
}
}, { dark: true });
@@ -73,5 +73,5 @@ export const overrideLightTheme = EditorView.theme({
".cm-line": {
color: "#333639"
},
});
+48 -23
View File
@@ -2,7 +2,8 @@
export interface DatabaseType {
name: string,
dialect: string;
logo: string
logo: string;
small_logo?: string
}
export enum DatabaseDialect {
@@ -16,47 +17,71 @@ export const DBTypes: DatabaseType[] = [
{
name: "Postgresql",
dialect: DatabaseDialect.POSTGRES,
logo: "/postgresql_logo.png"
logo: "/postgresql_logo.png",
small_logo: "/postgresql_logo_small.png"
}, {
name: "MySQL",
dialect: DatabaseDialect.MYSQL,
logo: "/mysql_logo.png"
logo: "/mysql_logo.png",
small_logo: "/mysql_logo_small.png"
},
{
name: "Sqlite",
dialect: DatabaseDialect.SQLITE,
logo: "/sqlite_logo.png"
logo: "/sqlite_logo.png",
small_logo: "/sqlite_logo_small.png"
},
{
{
name: "MariaDB",
dialect: DatabaseDialect.MARIADB,
logo: "/mariadb_logo.png"
logo: "/mariadb_logo.png",
small_logo: "/mariadb_logo_small.png"
},
]
export const getDatabaseByDialect = (dialect: DatabaseDialect): DatabaseType => {
const dbType : DatabaseType | undefined = DBTypes.find((dbType : DatabaseType) => dbType.dialect == dialect) ;
return dbType ? dbType : DBTypes[0] ;
const dbType: DatabaseType | undefined = DBTypes.find((dbType: DatabaseType) => dbType.dialect == dialect);
return dbType ? dbType : DBTypes[0];
}
export enum ImportMethodType {
DUMP = "DUMP" ,
DB_CLIENT = "DB_CLIENT" ,
DUMP = "DUMP",
DB_CLIENT = "DB_CLIENT",
}
export interface ImportDatabaseMethod {
id : string ;
name : string ;
logo? : string ;
icon? : React.ReactNode ;
instructions? : string ;
type : ImportMethodType ;
}
export interface ImportDatabaseOption {
dialect : DatabaseDialect ,
methods : ImportDatabaseMethod []
}
export interface ImportDatabaseMethod {
id: string;
name: string;
logo?: string;
icon?: React.ReactNode;
instruction?: string;
example?: string;
type: ImportMethodType;
numberOfInstructions?: number;
}
export interface ImportDatabaseOption {
dialect: DatabaseDialect,
methods: ImportDatabaseMethod[]
}
export const PG_DUMP_INSTRUCTIONS = "pg_dump -U [username] -d [database_name] -f [output_file.sql]";
export const PG_DUMP_EXAMPLE = "pg_dump -U root -d example_db -f example_db.sql";
export const MYSQL_DUMP_INSTRUCTIONS = "mysqldump -u [username] -p --no-data [database_name] > [output_file.sql]";
export const MYSQL_DUMP_EXAMPLE = "mysqldump -u root -p --no-data example_db > example_db.sql";
export const MARIADB_DUMP_INSTRUCTIONS = "mariadb-dump -u [username] -p --no-data [database_name] > [output_file.sql]";
export const MARIADB_DUMP_EXAMPLE = "mariadb-dump -u root -p --no-data example_db > example_db.sql";
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";
@@ -16,8 +16,11 @@ interface DbControlButtons {
const ZOOM_DURATION = 100
const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions }) => {
const { zoomIn, zoomOut, fitView } = useReactFlow();
const [zoom, setZoom] = useState<string>();
const { zoomIn, zoomOut, fitView , getZoom} = useReactFlow();
console.log ()
const [zoom, setZoom] = useState<string>(
`${Math.round(getZoom() * 100)}%`
);
const { t } = useTranslation();
const { undo, redo, canRedo, canUndo } = useDatabaseHistory();
@@ -71,7 +74,7 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
>
<Undo className={cn(
"size-4 dark:text-white",
!canUndo ? "text-font/30" : ""
!canUndo ? "text-font/30 dark:text-font/30" : ""
)} />
</Button>
</span>
@@ -185,14 +188,13 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
>
<Redo className={cn(
"size-4 dark:text-white",
!canRedo ? "text-font/30" : ""
!canRedo ? "text-font/30 dark:text-font/30" : ""
)} />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("control_buttons.redo")}
<span className="ml-2 text-default-400">
Ctnl + Shift + Z
</span>
+82 -24
View File
@@ -1,7 +1,7 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
addEdge, Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, NodeChange, NodePositionChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
} from "@xyflow/react";
@@ -40,22 +40,31 @@ import { useTheme } from "next-themes";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
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";
const DatabasePage: React.FC = () => {
const { open } = useModal();
const syncStatus = usePowerSync();
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField } = useDatabase();
const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
// Node and edge state hooks
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress } = useDiagramOps();
@@ -69,6 +78,41 @@ const DatabasePage: React.FC = () => {
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
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, {
closable: false
})
}
else if (databases.length == 0) {
// there is no databases in the first place to select from ,
// we have to create a new one .
open(Modals.CREATE_DATABASE, {
closable: false
});
}
}, [database?.id, isLoading, isFetching , (syncStatus.currentStatus as any).options.hasSynced])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
@@ -98,8 +142,8 @@ const DatabasePage: React.FC = () => {
addToast({
title: t("db_controller.invalid_relationship.title"),
description: t("db_controller.invalid_relationship.description"),
color: "danger",
variant : "solid"
color: "danger",
variant: "solid"
});
}
@@ -108,6 +152,7 @@ const DatabasePage: React.FC = () => {
// Called when nodes are updated (position changes or removed)
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
change.type == "position" && !change.dragging
) as NodePositionChange[];
@@ -127,7 +172,7 @@ const DatabasePage: React.FC = () => {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange]);
}, [onNodesChange, database]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
@@ -137,7 +182,6 @@ const DatabasePage: React.FC = () => {
if (edgeRemoveChanges.length > 0) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange]);
@@ -170,9 +214,14 @@ const DatabasePage: React.FC = () => {
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
return (
<div className="w-full h-screen flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading || !(syncStatus.currentStatus as any).options.hasSynced) && <Loading />
}
<div className="flex max-w-full">
<DBController />
</div>
@@ -191,31 +240,37 @@ const DatabasePage: React.FC = () => {
defaultEdgeOptions={{
type: 'relationship-edge',
}}
// onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
minZoom={0.10}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none "
>
className="shadow-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md"
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
maskStrokeWidth={1}
nodeClassName={"fill-default-300 dark:fill-font/30"}
<Background className=" dark:bg-background-100" />
/>
<Background className="dark:bg-background-100 " />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
@@ -231,8 +286,7 @@ const DatabasePage: React.FC = () => {
isIconOnly
color="danger"
className="size-8 p-1 "
onPressEnd={puls}
>
onPressEnd={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
@@ -248,17 +302,17 @@ const DatabasePage: React.FC = () => {
}
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker type="one" direction="start" />
<CardinalityMarker type="one" direction="start" selected />
<CardinalityMarker type="numeric" cardinality="one" direction="start" />
<CardinalityMarker type="numeric" cardinality="one" direction="start" selected />
<CardinalityMarker type="one" direction="end" />
<CardinalityMarker type="one" direction="end" selected />
<CardinalityMarker type="numeric" cardinality="one" direction="end" />
<CardinalityMarker type="numeric" cardinality="one" direction="end" selected />
<CardinalityMarker type="many" direction="start" />
<CardinalityMarker type="many" direction="start" selected />
<CardinalityMarker type="numeric" cardinality="many" direction="start" />
<CardinalityMarker type="numeric" cardinality="many" direction="start" selected />
<CardinalityMarker type="many" direction="end" />
<CardinalityMarker type="many" direction="end" selected />
<CardinalityMarker type="numeric" cardinality="many" direction="end" />
<CardinalityMarker type="numeric" cardinality="many" direction="end" selected />
</defs>
</svg>
@@ -268,4 +322,8 @@ const DatabasePage: React.FC = () => {
}
export default DatabasePage;
export default DatabasePage;
@@ -3,6 +3,7 @@ import React, { 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";
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
@@ -16,17 +17,17 @@ const DBController: React.FC<Props> = ({ }) => {
const onResize = (event: any, params: any) => {
setWidth(params.size.width);
}
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full overflow-hidden "
onResize={onResize} className="min-h-full overflow-visible "
minConstraints={[512]}
axis="x"
handle={
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary active:border-primary transition-colors duration-200">
<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>
}
>
@@ -2,7 +2,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, Select, SelectItem, SharedSelection } from "@heroui/react";
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
@@ -19,17 +19,19 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
const { t } = useTranslation();
const changeCardinality = (keys: SharedSelection) => {
if (keys.anchorKey != relationship.cardinality) {
if (keys.anchorKey && (keys.anchorKey != relationship.cardinality)) {
editRelationship({
id: relationship.id,
cardinality: keys.anchorKey as Cardinality,
} as RelationshipInsertType);
setCardinality(keys as any);
}
setCardinality(keys as any);
}
const removeRelationship = () => {
@@ -90,17 +92,18 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
{t('db_controller.cardinality.name')}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="cardinality"
selectedKeys={cardinality}
onSelectionChange={changeCardinality}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
>
<SelectItem key={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
<SelectItem key={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
@@ -1,12 +1,14 @@
import { useRelationshipName } from "@/hooks/use-relationship-name";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import hash from "object-hash";
import { getDefaultRelationshipName } from "@/utils/relationship";
interface RelationshipAccordionHeaderProps {
@@ -14,13 +16,17 @@ interface RelationshipAccordionHeaderProps {
relationship: RelationshipType
}
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen, relationship }) => {
const defaultName : string = useMemo(() => {
return getDefaultRelationshipName(relationship);
}, [relationship])
const [editMode, setEditMode] = useState<boolean>(false);
const { name: defaultName } = useRelationshipName(relationship);
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
const { focusOnRelationship } = useDiagramOps();
@@ -48,10 +54,10 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
'tarnsition-all duration-200',
'tarnsition-all duration-200 text-icon hover:text-font/90',
isOpen ? "rotate-[90deg]" : ""
)}>
<ChevronRight className="size-4 text-icon" />
<ChevronRight className="size-4" />
</div>
{
editMode && <>
@@ -64,16 +70,20 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
onValueChange={setName}
onBlur={editRelationshipName}
type="text"
className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 text-sm"
onClick={(event) => event.stopPropagation()}
classNames={{
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
input: "font-semibold text-black dark:text-font"
}}
/>
<Button
variant="light"
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font "
className=" text-icon hover:bg-default hover:text-font/90 "
size="sm"
onPress={editRelationshipName}
isIconOnly
>
<Check className="size-4 text-icon dark:text-white" />
<Check className="size-4 " />
</Button>
</>
}
@@ -82,63 +92,68 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
!editMode && <>
<label
className="w-full truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
className=" flex-1 truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
>
{relationship.name ? relationship.name : defaultName}
</label>
<div className="hidden shrink-0 flex-row group-hover:flex">
<div className="flex w-fit shrink-0 items-center justify-stretch ">
<div className="hidden shrink-0 group-hover:block ">
<Button
size="sm"
isIconOnly
variant="light"
onPressEnd={() => focusOnRelationship(relationship.id, true)}
>
<Focus className="size-4 text-icon" />
</Button>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => setEditMode(true)}
>
<Pencil className="size-4 text-icon" />
</Button>
</div>
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
onPressEnd={() => focusOnRelationship(relationship.id, true)}
>
<EllipsisVertical className="size-4 text-icon" />
<Focus className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[160px]" >
<div className="w-full flex flex-col gap-2 ">
<h3 className="font-semibold text-sm text-font/90 p-2">
{t("db_controller.actions")}
</h3>
</div>
<hr className="border-divider" />
<Listbox aria-label="Actions" className="p-0 pb-1" >
<ListboxItem
key="delete"
className="text-danger"
color="danger"
onPressEnd={onDeleteRelationship}
endContent={<Trash className="size-4" />}
<Button
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
onPress={() => setEditMode(true)}
>
<Pencil className="size-4 " />
</Button>
</div>
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
>
{t("db_controller.delete")}
</ListboxItem>
</Listbox>
</PopoverContent>
</Popover>
<EllipsisVertical className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[160px]" >
<div className="w-full flex flex-col gap-2 ">
<h3 className="font-semibold text-sm text-font/90 p-2">
{t("db_controller.actions")}
</h3>
</div>
<hr className="border-divider" />
<Listbox aria-label="Actions" className="p-0 pb-1" >
<ListboxItem
key="delete"
className="text-danger"
color="danger"
onPressEnd={onDeleteRelationship}
endContent={<Trash className="size-4" />}
>
{t("db_controller.delete")}
</ListboxItem>
</Listbox>
</PopoverContent>
</Popover>
</div>
</>
}
</div>
@@ -10,23 +10,23 @@ 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 { getDefaultRelationshipName } from "@/hooks/use-relationship-name";
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";
const RelationshipController: React.FC = ({ }) => {
const { open } = useModal();
const { database } = useDatabase();
const { relationships: allRelationships } = database || { relationships : []};
const { relationships: allRelationships } = database || { relationships: [] };
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
@@ -34,8 +34,7 @@ const RelationshipController: React.FC = ({ }) => {
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
const { focusedRelationshipId } = useDiagram();
useEffect(() => setRelationships(allRelationships), [allRelationships]);
useEffect(() => searchRelationships(), [allRelationships]);
useEffect(() => {
if (focusedRelationshipId) {
@@ -67,8 +66,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([])) ;
} , [])
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -81,10 +84,8 @@ const RelationshipController: React.FC = ({ }) => {
variant="light"
className="size-8 p-0 text-icon hover:text-font/90"
isIconOnly
onPress={() =>
//setShowDBML((value) => !value)
console.log("hello world ")
}
onPressEnd={collapseAll}
>
<ListCollapse className="size-4" />
@@ -100,15 +101,14 @@ const RelationshipController: React.FC = ({ }) => {
<Input
ref={nameRef}
type="text"
size="sm"
radius="sm"
variant="faded"
placeholder={t("db_controller.filter")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
onKeyUp={searchRelationships}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default group-hover:border-primary ",
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
}}
/>
@@ -11,19 +11,45 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
import CircularDependencyAlert from "./circular-dependecy-alert";
import { addToast } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import Clipboard from "@/components/clipboard/clipboard";
import { TableType } from "@/lib/schemas/table-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
interface SqlPreviewProps {
tableFilterIds ? : string[]
}
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
const { database : currentDatabase } = useDatabase();
const database = useMemo(() => {
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 ])
const SqlPreview: React.FC = ({ }) => {
const { database } = useDatabase();
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
console.log ( tableFilterIds ) ;
useEffect(() => {
if (circularDependency)
addToast({
@@ -38,14 +64,22 @@ const SqlPreview: React.FC = ({ }) => {
return <CircularDependencyAlert error={circularDependency} />
else
return (
<div className="flex w-full h-full ">
<div className="flex w-full h-full relative">
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
<Clipboard
text={sqlCode}
/>
</div>
{
<CodeMirror
<CodeMirror
defaultValue={sqlCode}
value={sqlCode}
className="flex flex-1 w-full"
className="flex flex-1 w-full "
extensions={[sql()]}
readOnly
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
}
</div>
@@ -55,4 +89,4 @@ const SqlPreview: React.FC = ({ }) => {
export default React.memo(SqlPreview);
export default React.memo(SqlPreview) ;
@@ -76,7 +76,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
const defaultValueType: DefaultValueType = useMemo(() => {
return {
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year" ,
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year",
boolean: field.type?.type == DataTypes.BOOLEAN,
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
@@ -251,7 +251,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
placeholder={t("db_controller.field_settings.value")}
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 ",
}}
/>
}
@@ -274,15 +274,20 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
defaultValueType.time &&
<>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="Time"
selectedKeys={timeSelection}
onSelectionChange={changeTimeDefaultValue}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
>
<SelectItem key={TimeDefaultValues.NO_VALUE}>{t("db_controller.field_settings.time_default_value.no_value")}</SelectItem>
<SelectItem key={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
@@ -310,15 +315,22 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
onChange={setDefaultDateTime}
granularity={field.type.name == "date" ? "day" : "second"}
hideTimeZone
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
inputWrapper: "border-divider group-hover:border-primary focus-within:border-primary",
}}
calendarProps={{
classNames: {
title: " text-font/90"
}
title: "text-font/90",
header : "bg-background" ,
} ,
}}
/>
</>
@@ -333,8 +345,9 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
endContent={
<Clock className="text-icon size-4" />
}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
inputWrapper: "border-divider group-hover:border-primary focus-within:border-primary",
}}
onBlur={saveDefaultDateTime}
@@ -348,8 +361,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
}
{
(defaultValueType.select || defaultValueType.multiSelect) &&
<Select
className="w-full"
<Select
size="sm"
variant="bordered"
aria-label="Enum Values"
@@ -357,8 +369,13 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
selectedKeys={selectedValues}
onSelectionChange={enumValueChange}
selectionMode={!defaultValueType.multiSelect ? 'single' : "multiple"}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
>
{
@@ -377,4 +394,9 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
}
export default React.memo(fieldDefautlValue);
export default React.memo(fieldDefautlValue);
@@ -2,12 +2,12 @@
import { useSortable } from "@dnd-kit/sortable";
import { Button, Input, Popover, PopoverContent, PopoverTrigger, Switch, Textarea } from "@heroui/react";
import { Ellipsis, EllipsisVertical, GripVertical, KeyRound, Settings, Settings2 } from "lucide-react";
import { Ellipsis, EllipsisVertical, GripVertical, KeyRound, Settings, Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import Autocomplete from "@/components/auto-complete/auto-complete";
import ToggleButton from "@/components/toggle/toggle";
import FieldSetting from "./field-setting";
@@ -22,8 +22,8 @@ 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 { editField } = useDatabaseOperations();
const { grouped_data_types } = useDatabaseOperations();
const { editField } = useDatabaseOperations();
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
const { t } = useTranslation();
@@ -42,7 +42,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
setSelectedType(field.typeId as string | undefined);
}, [field.typeId])
const saveFieldName = () => {
editField({
id: field.id,
@@ -50,11 +50,14 @@ const FieldItem: React.FC<Props> = ({ field }) => {
} as FieldType);
}
const updateFieldType = (key: Key | null) => {
editField({
id: field.id,
typeId: key
} as FieldType);
setSelectedType(key as string | undefined);
if (key != null) {
editField({
id: field.id,
typeId: key
} as FieldType);
setSelectedType(key as string | undefined);
}
}
const toggleNullable = (nullable: boolean) => {
@@ -86,14 +89,15 @@ const FieldItem: React.FC<Props> = ({ field }) => {
value={fieldName}
onValueChange={setFieldName}
onBlur={saveFieldName}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
}}
/>
<Autocomplete
items={grouped_data_types}
onSelectionChange={updateFieldType}
grouped
grouped
selectedItem={selectedType}
placeholder={t("db_controller.type")}
/>
@@ -116,7 +120,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<KeyRound className="size-4" />
</ToggleButton>
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
@@ -128,7 +132,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
</Button>
</PopoverTrigger>
<PopoverContent >
<FieldSetting field={field}/>
<FieldSetting field={field} />
</PopoverContent>
</Popover>
</div>
@@ -23,7 +23,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
const { t } = useTranslation();
const [fields, setFields] = useState<FieldType[]>(tableFields);
const { createField, orderTableFields } = useDatabaseOperations();
const { createField, orderTableFields , getInteger } = useDatabaseOperations();
useEffect(() => {
@@ -59,6 +59,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
tableId: tableId,
sequence: getNextSequence(fields) ,
nullable: true,
typeId: getInteger()?.id
})
}
return (
@@ -272,7 +272,14 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.unique")}
</span>
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
<Checkbox
defaultSelected={field.unique as boolean}
size="md"
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
onValueChange={toggleUnqiue} />
</div>
}
{
@@ -287,7 +294,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.autoIncrement")}
</span>
<Checkbox defaultSelected={field.autoIncrement as boolean} size="md" onValueChange={toggleAutoIncrement} />
<Checkbox defaultSelected={field.autoIncrement as boolean}
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
size="md" onValueChange={toggleAutoIncrement} />
</div>
}
{
@@ -296,7 +307,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.unsigned")}
</span>
<Checkbox defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
<Checkbox
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
</div>
}
{
@@ -305,7 +320,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.zeroFill")}
</span>
<Checkbox defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
<Checkbox
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
</div>
}
@@ -356,7 +375,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
placeholder={t("db_controller.field_settings.precision")}
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 ",
}}
/>
@@ -400,7 +419,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
placeholder={t("db_controller.field_settings.scale")}
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 ",
}}
/>
@@ -422,16 +441,21 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{t("db_controller.field_settings.charset")}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="charset"
placeholder={t("db_controller.field_settings.charset")}
selectedKeys={charset as any}
onSelectionChange={changeCharset}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
>
{
Object.values(charsets).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
@@ -445,15 +469,18 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{t("db_controller.field_settings.collation")}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="collation"
placeholder={t("db_controller.field_settings.collation")}
selectedKeys={collation as any}
onSelectionChange={changeCollation}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
>
{
@@ -503,9 +530,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}
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 ",
}}
/>
</>
}
@@ -539,9 +566,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
onValueChange={setNote}
onBlur={updateFieldNote}
classNames={{
inputWrapper: "bg-default border-divider ",
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"
}} />
<hr className="border-divider" />
<Button
@@ -67,14 +67,18 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
return (
<div className="flex gap-2 w-full">
<Select
className="w-full"
placeholder={t("db_controller.select_fields")}
selectionMode="multiple"
size="sm"
aria-label={t("db_controller.select_fields")}
variant="bordered"
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary",
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
onSelectionChange={onInexFieldChange}
@@ -122,8 +126,9 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
placeholder={t("db_controller.index_name")}
onBlur={editIndexName}
size="sm"
autoFocus
classNames={{
inputWrapper: "border-divider group-hover:border-primary ",
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
}}
defaultValue={index.name}
/>
@@ -140,7 +145,7 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
<span className="font-medium text-sm">
{t("db_controller.delete_index")}
</span>
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
</Button>
</div>
</PopoverContent>
@@ -8,7 +8,7 @@ import ColorPicker from "@/components/color-picker/color-picker";
import FieldList from "./field/field-list";
import IndexesList from "./index/indexes-list";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { getNextSequence } from "@/utils/field";
import { v4 } from "uuid";
import { IndexInsertType } from "@/lib/schemas/index-schema";
@@ -16,14 +16,16 @@ import { IndexInsertType } from "@/lib/schemas/index-schema";
export interface TableAccordionBodyProps {
table: TableType,
keys?: string[];
}
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table, keys }) => {
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
const [note, setNote] = useState<string>(table.note ? table.note : "");
const { t } = useTranslation();
const { editTable, createField, createIndex } = useDatabaseOperations();
const { editTable, createField, createIndex, getInteger } = useDatabaseOperations();
const onColorChange = useCallback((color: string | undefined) => {
editTable({ id: table.id, color: color ? color : null } as TableType);
@@ -38,7 +40,8 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
tableId: table.id,
sequence: getNextSequence(table.fields),
nullable: true,
})
typeId: getInteger()?.id
});
}
const addIndex = (event: any) => {
@@ -49,6 +52,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
unique: true,
tableId: table.id
} as IndexInsertType);
}
const saveNote = () => {
editTable({
@@ -63,7 +67,13 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
}, [table.note]);
useEffect(() => {
setSelectedKeys(new Set([...selectedKeys, "fields"]))
}, [table.fields.length])
useEffect(() => {
setSelectedKeys(new Set([...selectedKeys, "indexes"]))
}, [table.indices.length])
return (
<div className="w-full dark:bg-background-50">
@@ -149,7 +159,8 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
}>
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")}
classNames={{
inputWrapper: "bg-default border-divider dark:bg-background-100 ",
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"
}}
value={note}
onValueChange={setNote}
@@ -21,7 +21,7 @@ export interface TableAccordionHeaderProps {
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const { editTable, deleteTable, createField, createTable , createIndex } = useDatabaseOperations();
const { editTable, deleteTable, createField, createTable, createIndex , getInteger } = useDatabaseOperations();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
@@ -30,12 +30,12 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagramOps();
useEffect(() => {
setTableName(table.name);
}, [table.name])
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName } as TableInsertType);
setEditMode(false);
}, [tableName])
@@ -53,12 +53,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
tableId: table.id,
sequence: getNextSequence(table.fields),
nullable: true,
typeId : getInteger()?.id
})
}
const addIndex = (event: any) => {
setPopOverOpen(false)
createIndex({
id: v4(),
name: `index_${table.indices.length + 1}`,
@@ -84,10 +85,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
}}
>
<div className={cn(
'tarnsition-all duration-200',
'tarnsition-all duration-200 text-icon hover:text-font/90',
isOpen ? "rotate-[90deg]" : ""
)}>
<ChevronRight className="size-4 text-icon " />
<ChevronRight className="size-4 " />
</div>
<div className=" w-[1px] h-full bg-divider ">
</div>
@@ -99,6 +100,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<label
className="w-full text-editable truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
onDoubleClick={() => setEditMode(true)}
onClick={(event) => event.stopPropagation()}
>
{tableName}
</label>
@@ -113,23 +115,27 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
editMode && <>
<Input
placeholder={"Table name"}
autoFocus
size="sm"
value={tableName}
onChange={(event: any) => setTableName(event.target.value)}
variant="bordered"
onBlur={saveTableName}
autoFocus
type="text"
className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 text-sm"
onClick={(event) => event.stopPropagation()}
classNames={{
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
input: "font-semibold text-black dark:text-font"
}}
/>
<Button
variant="light"
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font "
className=" text-icon hover:bg-default hover:text-font/90 "
size="sm"
onPressEnd={saveTableName}
isIconOnly
>
<Check className="size-4 text-icon " />
<Check className="size-4 " />
</Button>
</>
}
@@ -143,17 +149,19 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
onPress={() => setEditMode(true)}
>
<Pencil className="size-4 text-icon " />
<Pencil className="size-4 " />
</Button>
<Button
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
onPressEnd={() => focusOnTable(table.id, true)}
>
<Focus className="size-4 text-icon " />
<Focus className="size-4 " />
</Button>
</div>
@@ -163,8 +171,9 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
size="sm"
isIconOnly
variant="light"
className="text-icon hover:bg-default hover:text-font/90"
>
<EllipsisVertical className="size-4 text-icon " />
<EllipsisVertical className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[160px]" >
@@ -180,19 +189,19 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
onPressEnd={addField}
className="text-font/90"
endContent={
<FileType className="size-4 " />
<FileType className="size-4 text-icon group-hover:text-font/90 " />
}>
{t("db_controller.add_field")}
</ListboxItem>
<ListboxItem
key="add_index"
className="text-font/90"
endContent={<FileKey className="size-4" />}
endContent={<FileKey className="size-4 text-icon group-hover:text-font/90" />}
showDivider
onPressEnd={addIndex}
>
>
{t("db_controller.add_index")}
</ListboxItem>
@@ -200,10 +209,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<ListboxItem
key="duplicate"
showDivider
className="text-font/90"
onPressEnd={duplicate}
endContent={<Copy className="size-4" />}>
endContent={<Copy className="size-4 text-icon group-hover:text-font/90" />}>
{t("db_controller.duplicate")}
</ListboxItem>
@@ -212,8 +221,8 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
className="text-danger"
color="danger"
onPressEnd={onDeleteTable}
endContent={<Trash className="size-4" />}
endContent={<Trash className="size-4 " />}
>
{t("db_controller.delete_table")}
</ListboxItem>
@@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
import { Code, List, Table } from "lucide-react"
import { useTranslation } from "react-i18next";
import { Ref, useCallback, useEffect, useRef, useState } from "react";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
@@ -12,16 +12,17 @@ import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
interface Props { }
const PADDING_X = 40;
const PADDING_Y = 80;
const TablesController: React.FC<Props> = ({ }) => {
const { database, getDefaultPrimaryKeyType } = useDatabase();
const { createTable, data_types } = useDatabaseOperations();
const TablesController: React.FC = ({ }) => {
const { database } = useDatabase();
const { createTable, getInteger } = useDatabaseOperations();
const { getViewport } = useReactFlow();
const { tables: allTables } = database || { tables : []};
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
@@ -29,14 +30,16 @@ const TablesController: React.FC<Props> = ({ }) => {
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId } = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const accordionRef = useRef<HTMLDivElement>(null);
useEffect(() => setTables(allTables), [allTables]);
useEffect(() => searchTables(), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
const viewport = getViewport();
const { x, y, zoom } = viewport;
// Convert screen (0,0) to flow coordinates using viewport values
const posX = -x / zoom + (PADDING_X / zoom);
const posY = -y / zoom + (PADDING_Y / zoom);
@@ -50,18 +53,25 @@ const TablesController: React.FC<Props> = ({ }) => {
name: "id",
isPrimary: true,
unique: true,
typeId: getDefaultPrimaryKeyType(database?.dialect)?.id
typeId: getInteger()?.id,
autoIncrement: true,
}]
} as TableInsertType);
setSelectedTable(new Set([newTableId]) as any);
}, [database , tables, getViewport, getDefaultPrimaryKeyType]);
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
setSelectedTable(new Set([focusedTableId]) as any);
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
})
}
}, [focusedTableId]);
@@ -73,10 +83,13 @@ const TablesController: React.FC<Props> = ({ }) => {
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
}, [nameRef, allTables]);
const toggleSqlPreview = useCallback(() => {
setShowSqlPreview(preview => !preview);
}, [])
}, []);
const tableFilterIds = useMemo(() => {
return tables.map((table: TableType) => table.id);
}, [tables]);
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -109,13 +122,12 @@ const TablesController: React.FC<Props> = ({ }) => {
ref={nameRef}
type="text"
size="sm"
autoFocus
radius="sm"
variant="faded"
placeholder={t("db_controller.filter")}
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",
}}
onKeyUp={searchTables}
@@ -130,29 +142,27 @@ const TablesController: React.FC<Props> = ({ }) => {
<Table className="h-4 w-4 " />
}
className="h-8 p-2 text-xs font-semibold"
//onClick={handleCreateTable}
> {t("db_controller.add_table")}
</Button>
</div>
{
!showSqlPreview &&
<div className=" flex-1 overflow-auto">
<Accordion
hideIndicator
selectedKeys={selectedTable}
onSelectionChange={setSelectedTable as any}
isCompact
ref={accordionRef}
>
{tables.map((table: TableType) => (
<AccordionItem
key={table.id}
id={table.id}
aria-label={table.name}
classNames={{
trigger: "w-full h-12 hover:bg-default transition-all duration-200 dark:hover:bg-background",
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
content: "bg-transparent"
}}
subtitle={
@@ -160,8 +170,7 @@ const TablesController: React.FC<Props> = ({ }) => {
isOpen={selectedTableId == table.id}
table={table}
/>
}
>
}>
<TableAccordionBody table={table} />
</AccordionItem>
))}
@@ -171,7 +180,9 @@ const TablesController: React.FC<Props> = ({ }) => {
{
showSqlPreview &&
<div className=" flex-1 overflow-auto ">
<SqlPreview />
<SqlPreview
tableFilterIds={tableFilterIds}
/>
</div>
}
@@ -2,19 +2,27 @@ import DatabaseCheckbox from "@/components/checkbox/database-checkbox";
import Modal, { ModalProps } from "@/components/modal/modal"
import { DatabaseType, DBTypes } from "@/lib/database";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Button, CheckboxGroup, Input } from "@heroui/react";
import { usePowerSync, usePowerSyncStatus } from "@powersync/react";
import { Database, SquareMenu } from "lucide-react";
import { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";
export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const [isValid, setIsValid] = useState<boolean>(false);
export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
const { t } = useTranslation();
const { isOpen, onOpenChange } = props ;
const [isValid, setIsValid] = useState<boolean>(false);
const [selectedDbType, setSelectedDbType] = useState<string[]>([DBTypes[0].dialect]);
const [dbName, setDbName] = useState<string>("db_example");
const { createDatabase, switchDatabase } = useDatabaseOperations();
const { open } = useModal();
const onDatabaseTypeChange = (types: string[]) => {
const selectedType: string | undefined = types.pop();
@@ -24,106 +32,101 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
const createNewDatabase = useCallback(async () => {
const databaseId: string = v4();
return new Promise(async (res, rej) => {
await createDatabase({
id: databaseId,
name: dbName,
dialect: selectedDbType[0] as any
});
switchDatabase(databaseId);
res(databaseId)
})
}, [selectedDbType, dbName])
try {
await createDatabase({
id: databaseId,
name: dbName,
dialect: selectedDbType[0] as any
});
switchDatabase(databaseId);
open(Modals.IMPORT_DATABASE);
res(databaseId);
} catch (error) {
rej(error);
}
})
}, [selectedDbType, dbName])
useEffect(() => {
setIsValid((selectedDbType.length > 0 && dbName.trim().length > 0) as boolean)
}, [selectedDbType, dbName])
}, [selectedDbType, dbName]);
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.pick_database")}
actionName={t("modals.continue")}
className="min-w-[720px]"
isDisabled={!isValid}
actionHandler={createNewDatabase}
header={t("modals.create_database_header")}
>
<div className="w-full justify-center flex ">
<div className="flex flex-col gap-1 w-[70%]">
<div className="p-8 py-2 pb-4 space-y-2">
<label className="text-sm text-font/90 font-semibold">
{t("modals.db_name")}
</label>
<Input
errorMessage={t("modals.db_name_error")}
isInvalid={dbName.trim().length == 0}
type="text"
value={dbName}
onValueChange={setDbName}
size="sm"
radius="sm"
variant="faded"
placeholder={t("modals.db_name")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
startContent={
<Database className="text-icon size-4 " />
}
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.pick_database")}
actionName={t("modals.continue")}
className="min-w-[720px]"
isDisabled={!isValid}
actionHandler={createNewDatabase}
header={t("modals.create_database_header")}
>
<div className="w-full justify-center flex ">
<div className="flex flex-col gap-1 w-[70%]">
<div className="p-8 py-2 pb-4 space-y-2">
<label className="text-sm text-font/90 font-semibold">
{t("modals.db_name")}
</label>
<Input
errorMessage={t("modals.db_name_error")}
isInvalid={dbName.trim().length == 0}
type="text"
value={dbName}
onValueChange={setDbName}
size="sm"
radius="sm"
variant="faded"
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 ",
}}
/>
</div>
<CheckboxGroup
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
base: "w-full p-0 ",
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
}}
/>
</div>
<CheckboxGroup
classNames={{
base: "w-full p-0 ",
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
}}
aria-label="Select Database"
value={selectedDbType}
onChange={onDatabaseTypeChange}
>
{
DBTypes.map((db: DatabaseType) => (
<DatabaseCheckbox
database={db}
/>
))
}
</CheckboxGroup>
<div className="p-8 py-4 space-y-2">
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
aria-label="Select Database"
value={selectedDbType}
onChange={onDatabaseTypeChange}
>
<SquareMenu className="size-4" /> Check examples
{
DBTypes.map((db: DatabaseType) => (
<DatabaseCheckbox
database={db}
/>
))
}
</CheckboxGroup>
<div className="p-8 py-4 space-y-2">
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<SquareMenu className="size-4" /> Check examples
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<span className="underline">
Empty Diagram
</span>
</Button>
</div>
>
<span className="underline">
Empty Diagram
</span>
</Button>
</div>
</div>
</div>
</Modal>
)
</Modal>
)
}
@@ -79,8 +79,8 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
id,
} as RelationshipInsertType);
onRlationshipCreated && onRlationshipCreated(id);
}, [relationship]);
onOpenChange && onOpenChange(false) ;
}, [relationship , onOpenChange]);
return (
@@ -92,7 +92,6 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
className="min-w-[520px]"
isDisabled={!isValid}
actionHandler={addRelationship}
>
<div className="grid grid-cols-2 grid-rows-2 gap-4">
@@ -14,7 +14,8 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
return await new Promise(async (res, rej) => {
await deleteDatabase(currentDatabaseId as string);
switchDatabase( undefined ) ;
res(currentDatabaseId)
res(currentDatabaseId) ;
onOpenChange && onOpenChange(false) ;
})
}, [currentDatabaseId])
+152 -110
View File
@@ -1,66 +1,109 @@
import Modal, { ModalProps } from "@/components/modal/modal"
import ReactCodeMirror, { oneDark } from "@uiw/react-codemirror";
import { useCallback, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { sql } from '@codemirror/lang-sql';
import { useTheme } from "next-themes";
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType } from "@/lib/database";
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 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 { adjustTablesPositions } from "@/utils/tables";
import Clipboard from "@/components/clipboard/clipboard";
import { Trans } from 'react-i18next';
const options: ImportDatabaseOption[] = [{
dialect: DatabaseDialect.POSTGRES,
methods: [{
id: "pg_dump",
name: "pg_dump",
icon: <Code className="size-4 text-font/90" />,
type: ImportMethodType.DUMP
}, {
id: "pg_admin",
name: "pg Admin",
logo: "/postgresql_logo.png",
type: ImportMethodType.DB_CLIENT
}]
}, {
dialect: DatabaseDialect.MYSQL,
methods: [{
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-4 text-font/90" />,
type: ImportMethodType.DUMP
}]
}
, {
dialect: DatabaseDialect.MARIADB,
methods: [{
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-4 text-font/90" />,
type: ImportMethodType.DUMP
}]
}]
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 } = useDatabase();
const { database , isLoading , isSwitchingDatabase } = useDatabase();
const { data_types, importDatabase } = useDatabaseOperations();
const [sqlCode, setSqlCode] = useState<string>("");
@@ -68,11 +111,11 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
}, [database]);
const [selectedMethodId, setSelectedMethodId] = useState<string[]>(currentOption ? [currentOption.methods[0].id] : []);
if (!currentOption) {
onOpenChange && onOpenChange(false);
}
const [selectedMethodId, setSelectedMethodId] = useState<string[]>([]);
useEffect(() => {
setSelectedMethodId(currentOption ? [currentOption.methods[0].id] : [])
}, [currentOption])
const onImportMethodChange = (types: string[]) => {
const selectedType: string | undefined = types.pop();
@@ -80,16 +123,21 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
setSelectedMethodId([selectedType]);
}
const selectedImportMethod: ImportDatabaseMethod = useMemo(() => {
const selectedImportMethod: ImportDatabaseMethod | undefined = useMemo(() => {
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId?.[0]) as ImportDatabaseMethod;
}, [selectedMethodId, currentOption])
const onImport = useCallback(async () => {
const { tables, relationships, indices } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect);
await importDatabase(tables, relationships, indices);
onOpenChange && onOpenChange(false);
return await importDatabase(tables, relationships, indices)
}, [sqlCode, database?.dialect, data_types]);
if (isLoading || isSwitchingDatabase)
return ;
return (
<Modal
@@ -102,7 +150,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
>
<div className="flex flex-col gap-4 ">
<div className="w-full ">
<p className="text-sm text-font/90">
<p className="text-sm text-font">
{t("modals.import_database.import_options")}
</p>
<CheckboxGroup
@@ -121,88 +169,82 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
value={method.id}
icon={method.icon}
logo={method.logo}
isSelected={selectedImportMethod?.id == method.id}
/>
))
}
</CheckboxGroup>
{
selectedImportMethod.type == ImportMethodType.DUMP &&
selectedImportMethod?.type == ImportMethodType.DUMP &&
<div className="space-y-2">
<label className="text-sm font-semibold">
Instructions :
<label className="text-sm font-medium ">
{t("import.instructions")}
</label>
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-2 marker:font-semibold ">
<ul className="list-decimal list-outside px-4 text-font text-sm space-y-2 marker:font-medium ">
<li>
install <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
{t("import.install")} <span className=" font-medium ">{selectedImportMethod?.name}</span> .
</li>
<li className="space-y-2">
<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" >
{selectedImportMethod.instruction}
<Clipboard text={selectedImportMethod.instruction} />
</CodeSection>
<li>
Run the following command in your terminal :
<ReactCodeMirror
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
editable={false}
value={`pg_dump -h <host> -p <port> -d <database_name>
-U <username> -s -F p -E UTF-8
-f <output_file_path>`}
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
/>
Example :
<ReactCodeMirror
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
value={`pg_dump -h localhost -p 5432 -d my_db
-U postgres -s -F p -E UTF-8
-f schema_export.sql`}
editable={false}
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
/>
<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" >
{selectedImportMethod.example}
<Clipboard text={selectedImportMethod.example} />
</CodeSection>
</li>
<li>
Drag and drop the output .sql file in code section or copy it content
{t("import.copy_code")}
</li>
</ul>
</div>
}
{
selectedImportMethod.type == ImportMethodType.DB_CLIENT &&
selectedImportMethod?.type == ImportMethodType.DB_CLIENT &&
<div className="space-y-2">
<label className="text-sm font-semibold">
Instructions :
<label className="text-sm font-medium">
{t("import.instructions")}
</label>
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-3 marker:font-semibold ">
<li>
Open <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
</li>
<li>
Right-click your database and select <span className="font-semibold text-font/90">Backup</span> from the context menu.
</li>
<li>
Name your <CodeSection className="p-1" color="default" size="sm"> .sql </CodeSection> file, set Format to <span className="font-semibold text-font/90">Plain</span>, and choose <span className="font-semibold text-font/90">Encoding: UTF8.</span>
</li>
<li>
Make sure <span className="font-semibold text-font/90">Only schema</span> is checked and <span className="font-semibold text-font/90">Only data</span> is unchecked in the <span className="font-semibold text-font/90">Data Options tab</span>.
</li>
<li>
Click <span className="font-semibold text-font/90">Backup</span> to export the file, then copy its content into the code editor section.
</li>
<ul className="list-decimal list-outside px-4 text-font text-sm space-y-3 marker:font-medium">
{
Array.from({ length: selectedImportMethod.numberOfInstructions as number }, (_, i) => i).map((index: number) => (
<li>
<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"
size="sm"
/>
}} />
</li>
))
}
</ul>
</div>
}
</div>
<div className="flex flex-1 ">
{
<ReactCodeMirror
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
extensions={[sql()]}
value={sqlCode}
onChange={setSqlCode}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
<ReactCodeMirror
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
extensions={[sql()]}
value={sqlCode}
onChange={setSqlCode}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
}
</div>
</div>
</Modal>
@@ -12,21 +12,23 @@ import { useTranslation } from "react-i18next";
const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
const { isOpen, onOpenChange } = props;
const { t } = useTranslation();
const { databases , currentDatabaseId} = useDatabase();
const { switchDatabase } = useDatabaseOperations();
const { databases, currentDatabaseId } = useDatabase();
const { switchDatabase } = useDatabaseOperations();
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(
(() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined)
);
const openDatabase = () => {
switchDatabase( selectedDatabase.currentKey)
switchDatabase(selectedDatabase.currentKey) ;
onOpenChange && onOpenChange(false) ;
}
return (
<Modal
{...props}
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.open_database")}
@@ -35,19 +37,18 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
actionHandler={openDatabase}
header={t("modals.open_database_header")}
isDisabled={!selectedDatabase?.size}
>
<Table
aria-label="Example static collection table"
color={"primary"}
selectionMode="single"
selectedKeys={selectedDatabase}
onSelectionChange={setSelectedDatabase}
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: "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 "
}}
>
<TableHeader className="rounded-sm">
@@ -62,8 +63,9 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
<TableRow key={database.id}>
<TableCell>
<Image
src={getDatabaseByDialect(database.dialect).logo}
src={getDatabaseByDialect(database.dialect).small_logo}
width={24}
radius="none"
/>
</TableCell>
<TableCell className="font-semibold">{database.name}</TableCell>
+4 -6
View File
@@ -46,11 +46,9 @@ const Field: React.FC<Props> = (props) => {
setEditMode(false);
}, [fieldName])
return (
<div className={cn(
"group relative flex h-8 items-center justify-between gap-1 border-t border-divider px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary/5 transition-all duration-200 ease-in-out",
"group relative flex h-8 items-center justify-between gap-1 border-t border-divider px-3 text-sm last:rounded-b-[6px] hover:bg-default dark:hover:bg-primary/10 transition-all duration-200 ease-in-out",
highlight ? "bg-primary/5" : ""
)}>
{
@@ -70,7 +68,7 @@ const Field: React.FC<Props> = (props) => {
<TooltipTrigger>
<MessageSquareQuote className="size-3.5 text-icon" />
</TooltipTrigger>
<TooltipContent>
<TooltipContent>
{field.note}
</TooltipContent>
</Tooltip>
@@ -111,12 +109,12 @@ const Field: React.FC<Props> = (props) => {
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent text-font/90"
className="rounded-sm outline-none px-2 py-0.5 w-full border-[0.5px] border-primary bg-content1 focus-visible:ring-0 text-sm dark:text-white"
/>
<Button
variant="light"
className="size-6 p-0 text-icon hover:text-font/90"
className="text-icon hover:bg-default hover:text-font/90"
size="sm"
isIconOnly
onPress={saveFieldName}
@@ -124,10 +124,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
markerEnd={`url(#${endMarker})`}
fill="none"
className={cn([
`!stroke-1 ${selected ? '!stroke-primary' : 'stroke-default-600 dark:stroke-default-300'}`,
])}
onClick={(e) => {
if (e.detail === 2) {
+3 -3
View File
@@ -114,11 +114,11 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
value={tableName}
onBlur={saveTableName}
type="text"
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent dark:text-white"
className="rounded-sm outline-none px-2 py-0.5 w-full border-[0.5px] border-primary font-bold bg-content1 focus-visible:ring-0 text-sm dark:text-white"
/>
<Button
variant="light"
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font/90"
className="text-icon hover:bg-default hover:text-font/90"
size="sm"
onPressEnd={saveTableName}
isIconOnly
@@ -149,7 +149,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
<Button
variant="light"
size="sm"
className="size-6 p-0 text-icon hover:bg-primary-foreground "
className="text-icon hover:bg-default hover:text-font/90 "
isIconOnly
onPressEnd={focus}
>
@@ -19,13 +19,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
useEffect(() => {
udpateDbFlag.current = false;
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
const databaseHash: string = hash(database, { algorithm: 'sha1' });
if (presentHash != databaseHash)
set(database);
if (datatbaseState.present && database) {
udpateDbFlag.current = false;
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
const databaseHash: string = hash(database, { algorithm: 'sha1' });
if (presentHash != databaseHash)
set(database);
}
}, [database]);
@@ -50,7 +50,8 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
return;
}
if ( ! database || !datatbaseState.present )
return ;
const normalizedDatabase = normalizeDatabase(database);
const normalizedPresent = normalizeDatabase(datatbaseState.present);
@@ -63,7 +64,7 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences);
(async () => {
try {
try {
await executeDbDiffOps(operations)
setIsProcessing(false);
}
@@ -19,9 +19,10 @@ interface DatabaseDataContextType {
currentDatabaseId: string | undefined,
databases: DatabaseType[],
isLoading: boolean,
isFetching : boolean ,
isSwitchingDatabase: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
getDefaultPrimaryKeyType : (dialect? : DatabaseDialect) => DataType | undefined
}
@@ -30,6 +31,8 @@ interface DatabaseDataContextType {
interface DatabaseOperationsContextType {
data_types: DataType[],
grouped_data_types : any ;
isSwitchingDatabase : boolean ;
getInteger : () => DataType | undefined ;
// database operations
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
@@ -17,7 +17,6 @@ import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-s
import { v4 } from "uuid";
import { DataType } from "@/lib/schemas/data-type-schema";
import { Modifiers } from "@/lib/field";
import { DatabaseDialect } from "@/lib/database";
@@ -27,12 +26,14 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(localStorage.getItem("database_id") as string | undefined);
// Fetch all databases
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
db.query.databases.findMany()
const { data: databases, isLoading: loadingDatabases, isFetching: fetchingDatabases } = useQuery(toCompilableQuery(
db.query.databases.findMany({
orderBy: desc(databaseModel.createdAt)
})
));
// Fetch the current database with nested tables, fields, and relationships
let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery(
let { data: database, isLoading: loadingCurrentDatabase, isFetching: fetchingDatabase } = useQuery(
toCompilableQuery(
db.query.databases.findMany({
where: (databases, { eq }) => eq(databases.id, currentDatabaseId as string),
@@ -84,49 +85,31 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
database = undefined as any;
// Fetch all data types
let { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
let { data: data_types, isLoading: loadingDataTypes, isFetching: fetchingDatatypes } = useQuery(toCompilableQuery(
db.query.data_types.findMany({
where: (data_types, { eq }) => eq(data_types.dialect, (database as any)?.dialect)
})
));
const charsetOrCollationDataTypes = data_types.filter((dataType: DataType) => {
const modifiers: string[] | undefined = dataType.modifiers ? JSON.parse(dataType.modifiers) : undefined;
if (modifiers) {
return modifiers.includes(Modifiers.COLLATE || Modifiers.CHARSET)
}
}).map((dataType: DataType) => dataType.name?.toUpperCase());
const grouped_data_types: any = useMemo(() => {
return groupBy(data_types, "type");
}, [data_types]);
// Auto-select first database if none is selected
useEffect(() => {
if (databases.length > 0 && !currentDatabaseId) {
switchDatabase(databases[0].id);
}
}, [currentDatabaseId, databases]);
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
const isSwitchingDatabase: boolean = useMemo(() => {
return isFetching && currentDatabaseId != (database as any)?.id
}, [currentDatabaseId, database, isFetching]);
const isFetching: boolean = fetchingDatabase || fetchingDatatypes || fetchingDatabases;
const isSwitchingDatabase: boolean = useMemo(() => {
return fetchingDatabase && currentDatabaseId != (database as any)?.id
}, [currentDatabaseId, database, fetchingDatabase]);
// CRUD for database
const createDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
return await db.insert(databaseModel).values({
...database,
createdAt: getTimestamp()
});
}, [db]);
const editDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
@@ -371,19 +354,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
}, [db, currentDatabaseId]);
const getDefaultPrimaryKeyType = useCallback((dialect?: DatabaseDialect) => {
if (dialect == DatabaseDialect.POSTGRES)
return data_types.find((dataType: DataType) => dataType.name == "bigserial");
else if (dialect == DatabaseDialect.SQLITE)
return data_types.find((dataType: DataType) => dataType.name == "integer");
return data_types.find((dataType: DataType) => dataType.name == "bigint");
const getInteger = useCallback(() => {
return data_types.find((dataType: DataType) => dataType.name == "integer");
}, [data_types]);
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
if (currentDatabaseId) {
return await db.transaction(async (tx) => {
@@ -428,7 +402,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [currentDatabaseId])
const databaseOpsValue = useMemo(() => ({
isSwitchingDatabase,
createDatabase,
editDatabase,
deleteDatabase,
@@ -453,8 +427,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editFieldIndices,
importDatabase,
data_types,
grouped_data_types
getInteger,
grouped_data_types,
}), [
isSwitchingDatabase,
createDatabase,
editDatabase,
deleteDatabase,
@@ -478,6 +454,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editFieldIndices,
importDatabase,
data_types,
getInteger,
grouped_data_types
]);
@@ -490,18 +467,23 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
databases: databases as DatabaseType[],
isLoading,
isSwitchingDatabase,
getDefaultPrimaryKeyType,
isFetching,
getField,
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
{
!database && !isLoading &&
/* !database && !isLoading &&
<>
{children}
</>
}
{
database && !isLoading && !isSwitchingDatabase &&
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>*/
}
{
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>
+16 -31
View File
@@ -5,7 +5,7 @@
.text-editable {
@apply dark:group-hover:bg-background group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary dark:group-hover:ring-divider rounded-sm cursor-pointer;
@apply dark:group-hover:bg-content1 group-hover:bg-background border-[0.5px] border-transparent group-hover:border-primary-300 dark:group-hover:ring-divider rounded-sm cursor-pointer;
}
@@ -39,6 +39,17 @@ body {
}
.selectable {
user-select: text;
-webkit-user-select: text;
/* Safari */
-moz-user-select: text;
/* Firefox */
-ms-user-select: text;
/* IE/Edge */
}
.react-flow__renderer {
cursor: default !important;
/* or pointer, grab, etc. */
@@ -118,16 +129,7 @@ div[data-slot="content"] hr[role="separator"] {
cursor: default !important;
}
svg.text-icon:hover {
color: #333639
}
.dark .bg-background-50 {
background-color: red !important;
}
.database-checkbox[data-selected="true"] {
@@ -148,14 +150,9 @@ thead[role="rowgroup"] th:last-child {
thead[role="rowgroup"] th:first-child {
border-radius: 4px 0px 0px 4px !important;
}
tbody[role="rowgroup"] tr td::before {
display: none;
}
@@ -202,11 +199,7 @@ button[data-testid="remove"]:after {
}
button[data-testid="remove"] svg {
display: none;
}
[data-invalid="true"] div[data-slot="input-wrapper"] {
@@ -215,16 +208,8 @@ button[data-testid="remove"] svg {
outline: none;
}
.option-checkbox[data-selected="true"] .option-span {
border-color: hsl(var(--heroui-divider));
div[data-slot="calendar"] div[data-slot="input-wrapper"]{
background-color: hsl(var(--heroui-default));
}
border : hsl(var(--heroui-divider))
}
+7
View File
@@ -71,4 +71,11 @@ export const getForeignRelationships = (table: TableType): RelationshipType[] =>
) : []);
return foreignRelationships;
}
export const getDefaultRelationshipName = (relationship: RelationshipType) => {
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
return "";
return `fk_${relationship.sourceTable?.name}_${relationship.targetTable?.name}`
}
+63 -31
View File
@@ -112,35 +112,66 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
}
}
} else {
for (const createTable of createTableStatements) {
let foreignKeyConstraints: any[] = [];
let referenceDefinitions: any[] = [];
for (let createTable of createTableStatements) {
try {
const instructionAst = parser.astify(createTable, {
database: getDatabaseByDialect(dialect).name
if (dialect == DatabaseDialect.SQLITE)
createTable = createTable.replace(/\btext\s*\(\s*\d+\s*\)/gi, 'TEXT');
let instructionAst = parser.astify(createTable, {
database: dialect == DatabaseDialect.MARIADB ? getDatabaseByDialect(DatabaseDialect.MYSQL).name : getDatabaseByDialect(dialect).name
});
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
console.log(instructionAst[0])
const table: TableInsertType = astToTable(instructionAst[0], data_types);
instructionAst = instructionAst[0];
}
console.log (instructionAst)
if (instructionAst) {
const table: TableInsertType = astToTable(instructionAst, data_types);
tables.push(table);
const foreignKeyConstraints = (instructionAst[0] as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
const referenceDefinitions = (instructionAst[0] as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
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);
foreignKeyConstraints = foreignKeyConstraints.concat(
tableForeignKeyConstraints.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
)
referenceDefinitions = referenceDefinitions.concat(
tableReferenceDefinitions.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
)
for (const foreignKeyConstraint of foreignKeyConstraints) {
relationships.push(astToRelationship(tables, table, foreignKeyConstraint) as RelationshipInsertType);
}
for (const referenceDefinition of referenceDefinitions) {
relationships.push(astToRelationship(tables, table, undefined, referenceDefinition) as RelationshipInsertType);
}
}
} catch (error) {
// console.log (error)
continue;
} catch (error) {
console.log(error)
continue;
}
}
for (const foreignKeyConstraint of foreignKeyConstraints) {
try {
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
} catch (error) {
console.log(error);
continue;
}
}
for (const referenceDefinition of referenceDefinitions) {
try {
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
} catch (error) {
console.log(error);
continue;
}
}
@@ -150,8 +181,10 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
database: getDatabaseByDialect(dialect).name
});
if (instructionAst) {
const targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == (instructionAst as any).table?.[0].table)
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, targetTable, undefined, undefined, instructionAst) as RelationshipInsertType[];
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
...instructionAst,
table: (instructionAst as any).table?.[0].table
}) as RelationshipInsertType[];
relationships = relationships.concat(extractedRelationships)
}
@@ -181,8 +214,6 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
}
return { tables, relationships, indices };
}
@@ -191,23 +222,28 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
export const astToRelationship = (tables: TableInsertType[], targetTable?: TableInsertType, constrainttAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
export const astToRelationship = (tables: TableInsertType[], constraintAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
let targetField: FieldInsertType | undefined;
let sourceTable: TableInsertType | undefined;
let sourceField: FieldInsertType | undefined;
let targetTable: TableInsertType | undefined;
let relationships: RelationshipInsertType[] = [];
if (constrainttAst) {
if (constraintAst) {
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constrainttAst.definition?.[0].column);
sourceTable = tables.find((table: TableInsertType) => table.name == constrainttAst.reference_definition?.table?.[0].table);
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constrainttAst.reference_definition?.definition?.[0].column);
targetTable = tables.find((table: TableInsertType) => table.name == constraintAst.table?.[0].table);
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.definition?.[0].column);
sourceTable = tables.find((table: TableInsertType) => table.name == constraintAst.reference_definition?.table?.[0].table);
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.reference_definition?.definition?.[0].column);
}
if (columnAst) {
targetTable = tables.find((table: TableInsertType) => table.name == columnAst.table?.[0].table);
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.column?.column);
sourceTable = tables.find((table: TableInsertType) => table.name == columnAst.reference_definition?.table?.[0].table);
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.reference_definition?.definition?.[0].column);
@@ -216,7 +252,7 @@ export const astToRelationship = (tables: TableInsertType[], targetTable?: Table
if (alterTableAst) {
const expressions = alterTableAst.expr;
const foreignKeyExpressions = expressions.filter((expression: any) => expression.resource == "constraint" && expression.create_definitions?.constraint_type == "FOREIGN KEY")
targetTable = tables.find((table: TableInsertType) => table.name == alterTableAst.table?.[0].table);
for (const expression of foreignKeyExpressions) {
const targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.definition?.[0].column);
@@ -265,7 +301,7 @@ export const astToRelationship = (tables: TableInsertType[], targetTable?: Table
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
console.log(ast)
return {
id: v4(),
name: ast.table[0]?.table,
@@ -295,10 +331,6 @@ export const astToField = (ast: any, data_types: DataType[], sequence: number):
let charset: string | undefined;
let collate: string | undefined;
let defaultValue: string | undefined = ast.default_val?.value?.value ? ast.default_val?.value?.value : undefined;
let maxLength: number | null = null;
+4 -12
View File
@@ -20,8 +20,6 @@ const adjustTablesPositions = async (
const tables: TableType[] = nodes.map((node: Node) => (
{ ...node.data.table as TableType }
)) as TableType[];
// Build the ELK graph structure
const graph = {
id: "root",
@@ -29,6 +27,7 @@ const adjustTablesPositions = async (
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
},
children: nodes.map((node) => ({
id: node.id,
@@ -40,28 +39,21 @@ const adjustTablesPositions = async (
sources: [rel.sourceTableId],
targets: [rel.targetTableId],
})),
};
};
// Run ELK layout (async)
const layoutedGraph = await elk.layout(graph);
// Map positions back to your tables
tables.forEach((table) => {
const node = layoutedGraph?.children?.find((n) => n.id === table.id);
if (node) {
// ELK positions are top-left, adjust to center like before
table.posX = node.x || 0 + (node.width / 2) + 112;
table.posY = node.y || 0 + (node.height / 2) + 75;
table.posX = (node.x || 0);
table.posY = (node.y || 0);
}
});
return tables;
};
const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
const tableAWidth: number = 224;
const tableAHeight: number = tableA.fields.length * 32 + 36;
+1 -1
View File
@@ -39,7 +39,7 @@ export default {
DEFAULT: "#cecfd2"
},
content1: {
DEFAULT: "#1c2026"
DEFAULT: "#20252c"
},
default: {