Merge remote-tracking branch 'origin/main' into copilot/dockerize-the-application

This commit is contained in:
KarimTamani
2026-03-29 14:22:50 +01:00
50 changed files with 956 additions and 640 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
tamanikarim50@gmail.com.
contact@stackrender.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+2 -2
View File
@@ -27,10 +27,10 @@ If you find a bug:
If you have an idea for improvement:
- Open a **feature request** in the issues section.
- Describe the feature and explain why it would be useful.
- You may also discuss ideas with the community on [Discord](https://discord.gg/DsN8RcPR6Y).
- You may also discuss ideas with the community on [Discord](https://discord.gg/4dv26jR4Pj).
## License Agreement
By contributing, you agree that your work will be licensed under the **GNU Affero General Public License v3.0**.
## Questions or Support
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/DsN8RcPR6Y).
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/4dv26jR4Pj).
+4 -4
View File
@@ -6,7 +6,7 @@
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
</a>
<a href="https://discord.com/invite/DsN8RcPR6Y">
<a href="https://discord.com/invite/4dv26jR4Pj">
<img src="https://img.shields.io/discord/1352085267535761448?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
</a>
<a href="https://x.com/intent/follow?screen_name=Iam_The_Dev">
@@ -85,8 +85,8 @@ npm run build
6. Export your Database SQL DDL and run it!
## 🤝 Community
- [**Discord**](https://discord.com/invite/DsN8RcPR6Y) Live discussions about upcoming releases and the future of StackRender.
- [**Docs**](https://www.stackrender.io/docs) Explore the StackRender docs to learn more about its features.
- [**Discord**](https://discord.com/invite/4dv26jR4Pj) Live discussions about upcoming releases and the future of StackRender.
- [**GitHub Issues**](https://github.com/stackrender/stackrender/issues) Report bugs and errors to help us improve your experience.
- [**X (Twitter)**](https://x.com/intent/follow?screen_name=Iam_The_Dev) Get the latest StackRender news and updates.
@@ -94,7 +94,7 @@ npm run build
We welcome all contributions, whether small bug fixes or major feature additions.
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/DsN8RcPR6Y) community.
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/4dv26jR4Pj) community.
- Follow the [Contributing Guide](./CONTRIBUTING.md) to get started.
- Agree to the [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure a positive and respectful environment.
+1 -1
View File
@@ -11,7 +11,7 @@ We currently provide security updates for the latest stable release of StackRend
## Reporting a Vulnerability
If you discover a security vulnerability in StackRender, please report it to us directly.
- Email: security@stackrender.com
- Email: contact@stackrender.io
- GitHub Security Advisory: Use the "Report a vulnerability" option in the repository's Security tab.
When reporting, please include:
+24 -37
View File
@@ -39,7 +39,6 @@
"@tabler/icons-react": "^3.34.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
"@types/react-resizable": "^3.0.8",
"@uiw/react-codemirror": "^4.23.12",
"@xyflow/react": "^12.6.3",
"class-variance-authority": "^0.7.1",
@@ -62,7 +61,7 @@
"react-day-picker": "^9.10.0",
"react-dom": "18.3.1",
"react-i18next": "^15.5.1",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "6.23.0",
"react-tag-input": "^6.10.6",
"sonner": "^2.0.7",
@@ -72,7 +71,8 @@
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",
"use-undo": "^1.1.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
@@ -81,7 +81,6 @@
"@types/pluralize": "^0.0.33",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-resizable": "^3.0.8",
"@typescript-eslint/eslint-plugin": "8.11.0",
"@typescript-eslint/parser": "8.11.0",
"@vitejs/plugin-react": "^4.2.1",
@@ -7610,16 +7609,6 @@
"@types/react": "*"
}
},
"node_modules/@types/react-resizable": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.8.tgz",
"integrity": "sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@@ -11814,6 +11803,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -12208,6 +12198,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -12441,31 +12432,14 @@
}
}
},
"node_modules/react-resizable": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
"integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
"node_modules/react-resizable-panels": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz",
"integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==",
"license": "MIT",
"dependencies": {
"prop-types": "15.x",
"react-draggable": "^4.0.3"
},
"peerDependencies": {
"react": ">= 16.3"
}
},
"node_modules/react-resizable/node_modules/react-draggable": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/react-router": {
@@ -13736,6 +13710,19 @@
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/vite": {
"version": "5.4.20",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
+4 -5
View File
@@ -1,7 +1,7 @@
{
"name": "StackRender",
"private": true,
"version": "1.0.1",
"version": "1.0.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -41,7 +41,6 @@
"@tabler/icons-react": "^3.34.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
"@types/react-resizable": "^3.0.8",
"@uiw/react-codemirror": "^4.23.12",
"@xyflow/react": "^12.6.3",
"class-variance-authority": "^0.7.1",
@@ -64,7 +63,7 @@
"react-day-picker": "^9.10.0",
"react-dom": "18.3.1",
"react-i18next": "^15.5.1",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "6.23.0",
"react-tag-input": "^6.10.6",
"sonner": "^2.0.7",
@@ -74,7 +73,8 @@
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",
"use-undo": "^1.1.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
@@ -83,7 +83,6 @@
"@types/pluralize": "^0.0.33",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-resizable": "^3.0.8",
"@typescript-eslint/eslint-plugin": "8.11.0",
"@typescript-eslint/parser": "8.11.0",
"@vitejs/plugin-react": "^4.2.1",
+6 -3
View File
@@ -64,9 +64,12 @@ export const Combobox = (props: ComboboxProps) => {
aria-expanded={open}
className={cn("justify-between", className)}
>
{value
? items.find((item) => item.id === value)?.[label]
: (placeholder ? placeholder : "Select items...")}
<label className="truncate">
{
value
? items.find((item) => item.id === value)?.[label]
: (placeholder ? placeholder : "Select items...")}
</label>
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
+22 -12
View File
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'
import { ThemeSwitch } from './theme-switch'
import RenameDb from '@/features/database/components/rename-db'
import LanguagesDropdown from './languages-dropdown'
import { useIsMobile } from '@/hooks/use-mobile'
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
fixed?: boolean
@@ -38,19 +39,13 @@ export const Header = ({
return () => document.removeEventListener('scroll', onScroll)
}, []);
const { open } = useModal();
const { t } = useTranslation();
const isMobile = useIsMobile();
return (
<header
className={cn(
'bg-background flex items-center gap-3 p-2 sm:gap-4 ',
'bg-background flex-col md:flex items-center gap-3 p-2 sm:gap-4 ',
fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md ',
offset > 10 && fixed ? 'shadow-sm' : 'shadow-none',
className
@@ -61,23 +56,38 @@ export const Header = ({
<div className="justify-self-start flex h-8 gap-3 items-center ">
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
<Separator orientation='vertical' className='h-6' />
<Menu />
{!isMobile && <Menu />}
</div>
<div className=" justify-self-center ">
<Search className='hidden md:flex' placeholder={t("navbar.search")} />
<Search className='hidden lg:flex' placeholder={t("navbar.search")} />
{
isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
</div>
<div className="justify-self-end flex gap-2">
<RenameDb />
{
!isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
<ThemeSwitch />
<LanguagesDropdown />
</div>
</div>
{isMobile && <div className='flex justify-center'>
<Menu />
</div>
}
</header>
)
}
@@ -10,9 +10,11 @@ import {
import { NavGroup } from '../nav-group'
import { useSidebarData } from './data/sidebar-data'
import { Separator } from '@/components/ui/separator';
import { useDiagramOps } from '@/providers/diagram-provider/diagram-provider';
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const sidebarData = useSidebarData();
const { openController } = useDiagramOps();
return (
<Sidebar collapsible='icon' variant='floating' {...props} className='bg-card border-r' >
<SidebarHeader>
@@ -24,12 +26,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<h3 className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground truncate font-semibold text-sm '>
StackRender
</h3>
</div>
</div>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((props: any, index: number) => (
<div key={props.title}>
<NavGroup key={props.title} {...props} />
<NavGroup key={props.title} {...props} onClick={() => openController(true)} />
{
index != sidebarData.navGroups.length - 1 &&
<div className='px-2'>
@@ -50,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</div>
}
</div>
))}
))}
</SidebarFooter>
<SidebarRail />
</Sidebar>
@@ -1,8 +1,6 @@
import {
IconBrandDiscord,
IconBrandGithub,
IconBrandX,
IconSparkles,
IconTableAlias,
IconVectorSpline,
} from '@tabler/icons-react'
@@ -10,6 +8,7 @@ import {
import type { SidebarData } from '../../types';
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { BookIcon } from 'lucide-react';
export const useSidebarData = () => {
@@ -37,16 +36,10 @@ export const useSidebarData = () => {
],
footerNavGroups:[ {
title: '',
items: [
{
title: "X",
url: "https://x.com/Iam_The_Dev",
icon: IconBrandX,
newTab : true
},
items: [
{
title: "Discord",
url: "https://discord.gg/DsN8RcPR6Y",
url: "https://discord.gg/4dv26jR4Pj",
icon: IconBrandDiscord,
newTab : true
},
@@ -56,6 +49,12 @@ export const useSidebarData = () => {
icon: IconBrandGithub,
newTab : true
},
{
title : t("sidebar.documentation") ,
url : "https://www.stackrender.io/docs" ,
icon : BookIcon,
newTab : true
}
],
}]
@@ -124,11 +124,9 @@ export const useMenuData = () => {
openController(!showController)
}
},
{
id: "menu.theme",
title: t("menu.theme"),
children: [
{
id: "light",
@@ -150,10 +148,16 @@ export const useMenuData = () => {
id: "menu.help",
title: t("menu.help"),
children: [
{
id: "menu.documentation", title: t("sidebar.documentation"),
clickHandler: () => {
window.open("https://www.stackrender.io/docs", "_blank")
}
},
{
id: "menu.join_discord", title: t("menu.join_discord"),
clickHandler: () => {
window.open("https://discord.gg/DsN8RcPR6Y", "_blank")
window.open("https://discord.gg/4dv26jR4Pj", "_blank")
}
},
],
+4 -3
View File
@@ -29,7 +29,7 @@ import {
} from '../ui/dropdown-menu'
import { NavCollapsible, NavItem, NavLink, NavGroup as NavGroupType } from './types.ts'
export function NavGroup({ title, items }: NavGroupType) {
export function NavGroup({ title, items , onClick }: NavGroupType) {
const { state, isMobile } = useSidebar();
const location = useLocation();
const href = location.pathname
@@ -41,7 +41,7 @@ export function NavGroup({ title, items }: NavGroupType) {
const key = `${item.title}-${item.url}`
if (!item.items)
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined}/>
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined} onClick={onClick}/>
if (state === 'collapsed' && !isMobile)
return (
@@ -59,7 +59,7 @@ const NavBadge = ({ children }: { children: ReactNode }) => (
<Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
)
const SidebarMenuLink = ({ item, href , target}: { item: NavLink; href: string; target?: string }) => {
const SidebarMenuLink = ({ item, href , target , onClick}: { item: NavLink; href: string; target?: string , onClick? : ()=> void }) => {
const { setOpenMobile } = useSidebar()
return (
<SidebarMenuItem>
@@ -67,6 +67,7 @@ const SidebarMenuLink = ({ item, href , target}: { item: NavLink; href: string;
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
onClick={onClick}
>
<Link to={item.url} onClick={() => setOpenMobile(false)} target={target}>
{item.icon && <item.icon />}
+8 -7
View File
@@ -1,5 +1,5 @@
interface BaseNavItem {
@@ -10,8 +10,8 @@ interface BaseNavItem {
type NavLink = BaseNavItem & {
url: string
items?: never;
newTab? : boolean
items?: never;
newTab?: boolean
}
type NavCollapsible = BaseNavItem & {
@@ -23,13 +23,14 @@ type NavItem = NavCollapsible | NavLink
interface NavGroup {
title: string
items: NavItem[]
items: NavItem[];
onClick?: () => void;
}
interface SidebarData {
footerNavGroups : NavGroup[]
footerNavGroups: NavGroup[]
navGroups: NavGroup[]
}
@@ -47,4 +48,4 @@ interface MenuItem {
clickHandler?: () => void
}
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink , MenuItem }
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink, MenuItem }
+1 -1
View File
@@ -67,7 +67,7 @@ const Modal: React.FC<ModalProps> = ({
{
footer &&
<DialogFooter >
<div className="flex justify-between w-full">
<div className="flex justify-between w-full flex-col gap-4 sm:flex-row">
{
+133
View File
@@ -0,0 +1,133 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
+56
View File
@@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
@@ -0,0 +1,351 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
} from "@xyflow/react";
// React built-ins
import { useCallback, useEffect, useMemo } from "react";
// Custom components and styles
import Table from "./table";
import '@xyflow/react/dist/style.css';
import Relationship from "./relationship";
import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
import { TARGET_PREFIX } from "./field";
import CardinalityMarker from "@/features/database/components/cardinality-marker";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { AlertTriangle, Menu } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import { Loading } from "@/components/layout/loading-modal";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import DatabaseControlButtons from "./database-control-buttons";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const DatabaseDiagram: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { theme: resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField, isSwitchingDatabase, isLoading, databases } = 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, cardinalityStyle, showController, openController } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
// Hook to allow zooming and centering the diagram
const { fitView } = useReactFlow();
// Define custom node and edge types
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
useEffect(() => {
if (isLoading)
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
});
} else {
fitView({
duration: 500
});
}
}, [database?.id, isLoading])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase, database?.id])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
await createRelationship({
id: v4(),
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
} else {
toast(t("db_controller.invalid_relationship.title"), {
description: t("db_controller.invalid_relationship.description"),
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
})
}
setIsConnectionInProgress(false);
}, [database, t]);
// 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[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange, database, isSwitchingDatabase]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange, isSwitchingDatabase]);
// When user starts connecting fields
const onConnectStart = useCallback(() => {
setIsConnectionInProgress(true);
}, []);
// When user ends connecting fields
const onConnectEnd = useCallback(() => {
setIsConnectionInProgress(false);
}, []);
// Automatically reposition tables to avoid overlap and fit the view
const adjustPositions = useCallback(async () => {
const adjustedTables = await adjustTablesPositions(nodes, relationships);
updateTablePositions(adjustedTables)
setTimeout(() => {
fitView({
duration: 500
});
}, 300);
}, [relationships, nodes]);
// Convert tables and relationships into flow elements
useTableToNode(tables);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
const isMobile = useIsMobile();
return (
<div className="w-full h-full flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading) && <Loading />
}
{
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
animated: false
}}
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
minZoom={0.10}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position={isMobile ? "top-center" : "bottom-center"}
showFitView={false}
showZoom={false}
showInteractive={false}
className="!shadow-none !border-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="!bg-background border-1 rounded-lg overflow-hidden "
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
maskStrokeWidth={1}
nodeClassName={"!fill-muted-foreground/20 "}
style={{
width: 164,
height: 128
}}
/>
<Background color="#62748e" className="dark:!bg-background " />
</ReactFlow>
<div
className={cn("absolute left-[24px] ", {
"bottom-[24px]": !isMobile,
"top-[24px]": isMobile
})}
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="destructive"
size="icon"
className="shadow-lg size-8"
onClick={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive ">
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
{
isMobile &&
<Button size={"icon"} className="absolute bottom-[24px] left-[24px]"
onClick={() => {
openController(!showController)
}}
>
<Menu className="size-5" />
</Button>
}
</div>
}
{
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
</defs>
</svg>
}
</div>
)
}
export default DatabaseDiagram;
@@ -1,68 +0,0 @@
import React, { useEffect, useState } from "react"
import { Outlet } from "react-router-dom"
import { ResizableBox as ResizableBoxRaw } from 'react-resizable';
import 'react-resizable/css/styles.css';
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
interface Props {
}
const DBController: React.FC<Props> = ({ }) => {
const [width, setWidth] = useState<number>(512);
const { showController } = useDiagramOps()
const [show, setShow] = useState<boolean>(!showController);
const [style, setStyle] = useState<any>(undefined);
const onResize = (event: any, params: any) => {
setWidth(params.size.width);
}
useEffect(() => {
if (!showController) {
setWidth(0);
setStyle({
transition: 'width 0.5s',
})
setTimeout(() => {
setShow(false);
}, 500);
} else if (showController) {
setShow(true);
setTimeout(() => setWidth(486));
setTimeout(() => {
setStyle(undefined);
}, 500)
}
}, [showController])
if (show)
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full overflow-hidden "
minConstraints={[486]}
axis="x"
style={style}
handle={
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary/50 active:border-primary transition-colors duration-200 ">
</div>
}
>
<div className="min-w-[486px] w-full h-full bg-background dark:bg-card border-r ">
<Outlet />
</div>
</ResizableBox>
)
else
return;
}
export default React.memo(DBController)
@@ -14,6 +14,7 @@ import { IconFilter2Up, IconPlus } from "@tabler/icons-react";
import { Accordion } from "@/components/ui/accordion";
import RelationshipAccordionItem from "./relationship-accordion-item/relationship-accordion-item";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
const RelationshipController: React.FC = ({ }) => {
@@ -22,32 +23,28 @@ const RelationshipController: React.FC = ({ }) => {
const { database } = useDatabase();
const { relationships: allRelationships } = database || { relationships: [] };
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [selectedRelationship, setSelectedRelationship] = useState<string | undefined>();
const { focusedRelationshipId } = useDiagram();
const { t } = useTranslation();
const { focusedRelationshipId , setFocusedRelationshipId } = useDiagram();
useEffect(() => searchRelationships(), [allRelationships]);
useEffect(() => {
if (focusedRelationshipId) {
setSelectedRelationship(focusedRelationshipId);
if (focusedRelationshipId) {
const accordionItem = document.getElementById(focusedRelationshipId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
behavior: 'smooth', block: "nearest"
})
}
}, [focusedRelationshipId]);
const onOpen = useCallback(() => {
open(Modals.CREATE_RELATIONSHIP, {
onRlationshipCreated: (id: string) => setSelectedRelationship(id)
onRlationshipCreated: (id: string) => setFocusedRelationshipId(id)
})
}, []);
const searchRelationships = useCallback(() => {
const keyword: string | undefined = nameRef.current?.value;
if (keyword !== undefined) {
@@ -66,13 +63,12 @@ const RelationshipController: React.FC = ({ }) => {
}, [nameRef, allRelationships]);
const collapseAll = useCallback(() => {
setSelectedRelationship("");
setFocusedRelationshipId(undefined);
}, []);
return (
<div className="w-full h-full flex flex-col pl-3 ">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
@@ -116,16 +112,19 @@ const RelationshipController: React.FC = ({ }) => {
{
allRelationships.length > 0 &&
<ScrollArea className=" flex-1 overflow-auto pr-3">
<ScrollArea className="flex-1 px-3 overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full "
value={selectedRelationship}
onValueChange={setSelectedRelationship}
value={focusedRelationshipId}
onValueChange={setFocusedRelationshipId}
>
{relationships.map((relationship: RelationshipType) => (
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<>
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<Separator className="my-1" />
</>
))}
</Accordion>
</ScrollArea>
@@ -133,7 +132,7 @@ const RelationshipController: React.FC = ({ }) => {
{
(allRelationships.length == 0) &&
<div className="pr-2 h-full">
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_relationships")}
description={t("db_controller.empty_list.no_relationships_description")}
@@ -15,7 +15,7 @@ interface RelationshipAccordionItemProps {
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
return (
<AccordionItem value={relationship.id} className="my-2" id={relationship.id}>
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
<RelationshipAccordionTrigger relationship={relationship} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<RelationshipAccordionContent
@@ -66,13 +66,13 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
</label>
</div>
<div className="hidden shrink-0 flex-row group-hover:flex gap-2">
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnRelationship(relationship.id, true)
}}>
@@ -92,16 +92,23 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
editRelationshipName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" onClick={editRelationshipName}>
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={editRelationshipName}>
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" >
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
@@ -9,8 +9,7 @@ import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
import EmptyList from "@/components/empty-list";
import { useModal } from "@/providers/modal-provider/modal-provider";
import EmptyList from "@/components/empty-list";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconListDetails, IconPlus } from "@tabler/icons-react";
@@ -28,6 +27,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { getTableNextSequence } from "@/utils/tables";
import { Separator } from "@/components/ui/separator";
const TablesController: React.FC = ({ }) => {
@@ -38,9 +38,9 @@ const TablesController: React.FC = ({ }) => {
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState<string | undefined>(undefined);
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId } = useDiagram();
const { focusedTableId , setFocusedTableId} = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const sensors = useSensors(
@@ -59,7 +59,7 @@ const TablesController: React.FC = ({ }) => {
const newIndex = items.findIndex((item: TableType) => item.id == over.id);
const tables = arrayMove(items, oldIndex, newIndex);
orderTables(tables)
return tables;
@@ -72,7 +72,7 @@ const TablesController: React.FC = ({ }) => {
useEffect(() => searchTables(), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
const viewport = getViewport();
const { x, y, zoom } = viewport;
@@ -96,17 +96,15 @@ const TablesController: React.FC = ({ }) => {
}]
} as TableInsertType);
setSelectedTable(newTableId);
setFocusedTableId(newTableId);
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
setSelectedTable(focusedTableId);
setShowSqlPreview(false);
if (focusedTableId) {
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
behavior: 'smooth', block: "nearest"
})
}
}, [focusedTableId]);
@@ -128,8 +126,8 @@ const TablesController: React.FC = ({ }) => {
}, [tables]);
return (
<div className="w-full h-full flex flex-col pl-3 ">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
@@ -174,13 +172,13 @@ const TablesController: React.FC = ({ }) => {
</div>
{
allTables.length > 0 && !showSqlPreview &&
<ScrollArea className="overflow-y-auto pr-3 h-full w-full ">
<ScrollArea className="px-3 h-full w-full overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full"
value={selectedTable}
onValueChange={setSelectedTable}
value={focusedTableId}
onValueChange={setFocusedTableId}
>
<DndContext
sensors={sensors}
@@ -194,7 +192,10 @@ const TablesController: React.FC = ({ }) => {
>
{tables.map((table: TableType) => (
<TableAccordionItem table={table} key={table.id} />
<>
<TableAccordionItem table={table} key={table.id} />
<Separator className="my-1" />
</>
))}
</SortableContext>
@@ -213,7 +214,7 @@ const TablesController: React.FC = ({ }) => {
}
{
(allTables.length == 0) &&
<div className="pr-3 h-full">
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_tables")}
description={t("db_controller.empty_list.no_tables_description")}
@@ -4,7 +4,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldType } from "@/lib/schemas/field-schema";
import { FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import FieldSetting from "./field-setting";
@@ -61,6 +61,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
if (!dataType)
return;
if (dataType.id == field.typeId)
return;
if (key != null) {
editField({
id: field.id,
@@ -98,6 +100,13 @@ const FieldItem: React.FC<Props> = ({ field }) => {
onChange={(event: any) => setFieldName(event.target.value)}
onBlur={saveFieldName}
className=" flex flex-1 !bg-transparent"
onKeyDown={(e : any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName() ;
e.target.blur() ;
}
}}
/>
<Combobox
items={data_types}
@@ -105,7 +114,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
placeholder={t("db_controller.type")}
selectedItem={selectedType}
onSelectionChange={updateFieldType}
className="flex flex-1 !bg-transparent !font-normal"
className="flex flex-1 !bg-transparent !font-normal min-w-0"
/>
</div>
<div className="flex gap-2 ml-2 ">
@@ -113,8 +122,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
// className="size-9 rounded-md bg-secondary data-[state=on]:bg-background dark:data-[state=on]:bg-card data-[state=on]:border-1 text-muted-foreground data-[state=on]:text-foreground/75 "
className="size-9 text-muted-foreground data-[state=on]:border-1"
className="size-9 text-muted-foreground "
pressed={!field.nullable as boolean}
onPressedChange={toggleNullable}
>
@@ -130,7 +139,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground data-[state=on]:border-1"
className="size-9 text-muted-foreground "
pressed={field.isPrimary as boolean}
onPressedChange={togglePrimaryKey}
>
@@ -91,7 +91,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
>
<AccordionItem value="fields" className="border-none">
<AccordionTrigger className="py-1 rounded-none text-muted-foreground " position="right" >
<AccordionTrigger className="py-1 rounded-none text-muted-foreground pr-1.5" position="right" >
<div className="w-full flex gap-1 items-center">
<IconFolder className="size-4" />
{t("db_controller.fields")}
@@ -103,7 +103,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
<AccordionItem value="indexes" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground " position="right">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconBolt className="size-4" />
{t("db_controller.indexes")}
@@ -115,7 +115,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
<AccordionItem value="note" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground " position="right">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconMessageCircle className="size-4 " />
{t("db_controller.note")}
@@ -133,7 +133,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
</Accordion>
<div className="flex pt-2 justify-between items-center ">
<div className="flex pt-2 justify-between pr-1.5 items-center ">
<div className="h-full flex items-center">
<ColorPicker
defaultColor={table.color as string}
@@ -28,12 +28,12 @@ const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
return (
<AccordionItem value={table.id} className="my-2 border-none" id={table.id} ref={ref}>
<AccordionItem value={table.id} className=" border-none" id={table.id} ref={ref}>
<div
ref={setNodeRef}
style={style}
{...attributes}
className="w-full border-b "
className="w-full "
>
<TableAccordionTrigger table={table} />
<AccordionContent className="flex flex-col gap-4 text-balance">
@@ -126,14 +126,14 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
</Tooltip>
</div>
<div className="hidden !shrink-0 flex-row group-hover:flex gap-2 ">
<Button variant="outline" size="icon" className="size-7 shrink-0" onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0" onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnTable(table.id, true)
}}>
@@ -154,9 +154,16 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" onClick={saveTableName} >
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={saveTableName} >
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
@@ -164,7 +171,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-7 shrink-0 flex rounded-sm" >
<Button variant="ghost" size="icon" className="size-8 shrink-0 flex rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
@@ -98,6 +98,13 @@ const Field: React.FC<Props> = (props) => {
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="h-6 font-bold pb-1.5 rounded-sm px-1"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName();
e.target.blur();
}
}}
/>
}
@@ -46,7 +46,7 @@ const RenameDB: React.FC = ({ }) => {
});
setIsLoading(false);
setEditMode(false);
}
}
if (!database)
@@ -59,7 +59,7 @@ const RenameDB: React.FC = ({ }) => {
<Tooltip >
<TooltipTrigger asChild>
<label
className=" w-full max-w-[256px] truncate h-8 text-sm font-medium flex gap-4 items-center justify-center cursor-pointer"
className=" w-full max-w-[256px] truncate h-8 text-sm font-medium flex gap-4 items-center justify-center cursor-pointer"
onDoubleClick={() => setEditMode(true)}
>
<img
@@ -95,7 +95,7 @@ const RenameDB: React.FC = ({ }) => {
value={dbName}
onBlur={saveDatabaseName}
autoFocus
className="h-8"
className="h-8 min-w-[164px]"
/>
<Button
variant="default"
+12 -5
View File
@@ -4,7 +4,7 @@ import { Edge, Node, NodeProps } from "@xyflow/react";
import hash from 'object-hash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Check,
Check,
ChevronUp,
ChevronDown,
} from 'lucide-react';
@@ -15,14 +15,12 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
import { useTranslation } from "react-i18next";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Card, CardContent , CardHeader } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react";
import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export type TableProps = Node<{
table: TableType,
overlapping?: boolean,
@@ -102,6 +100,8 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
}
: undefined
}
onDoubleClick={focus}
>
<CardHeader className="group rounded-t rounded-t-md p-1.5 flex items-center mb-0 bg-primary/10 "
@@ -142,6 +142,13 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
value={tableName}
onBlur={saveTableName}
style={{ color: table.color as string }}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
e.target.blur();
}
}}
/>
<div className="flex gap-1 shrink-0 flex-row transition-opacity duration-200">
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
+37
View File
@@ -0,0 +1,37 @@
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { cn } from "@/lib/utils";
import { Outlet } from "react-router-dom";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import DatabaseDiagram from "./components/database-diagram";
export const DatabaseDesktopLayout: React.FC = () => {
const { showController } = useDiagramOps()
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
minSize={25}
maxSize={showController ? 99 : 0}
className={cn('transition-[flex-grow] duration-500 bg-card ', {
"min-w-[460px]": showController
})}
>
<div className="min-w-[460px] h-full">
<Outlet />
</div>
</ResizablePanel>
<ResizableHandle
disabled={!showController}
className={cn("h-full cursor-ew-resize hover:bg-primary/50 active:border-primary transition-colors duration-200", !showController ? 'hidden' : '')}
/>
<ResizablePanel defaultSize={75} >
<DatabaseDiagram/>
</ResizablePanel>
</ResizablePanelGroup>
);
};
export default DatabaseDesktopLayout;
+22 -328
View File
@@ -1,340 +1,34 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
} from "@xyflow/react";
// React built-ins
import { useCallback, useEffect, useMemo } from "react";
// Custom components and styles
import Table from "./components/table";
import '@xyflow/react/dist/style.css';
import Relationship from "./components/relationship";
import DBController from "./components/db-controller";
import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
import { TARGET_PREFIX } from "./components/field";
import CardinalityMarker from "@/features/database/components/cardinality-marker";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { AlertTriangle } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import React, { Suspense } from "react";
import { Loading } from "@/components/layout/loading-modal";
import { usePowerSync } from "@powersync/react";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import DatabaseControlButtons from "./components/database-control-buttons";
import RenameDb from "./components/rename-db";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useIsMobile } from "@/hooks/use-mobile";
const DatabaseDesktopLayout = React.lazy(
() => import('./desktop-layout')
);
const Database: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { theme: resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField, isSwitchingDatabase, isLoading, databases } = 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, cardinalityStyle } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
// Hook to allow zooming and centering the diagram
const { fitView } = useReactFlow();
// Define custom node and edge types
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
const DatabaseMobileLayout = React.lazy(
() => import('./mobile-layout')
);
export const DatabaseLayout: React.FC = () => {
useEffect(() => {
if (isLoading)
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
});
} else {
fitView({
duration: 500
});
}
}, [database?.id, isLoading])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase, database?.id])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
await createRelationship({
id: v4(),
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
} else {
toast(t("db_controller.invalid_relationship.title"), {
description: t("db_controller.invalid_relationship.description"),
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
})
}
setIsConnectionInProgress(false);
}, [database, t]);
// 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[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange, database, isSwitchingDatabase]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange, isSwitchingDatabase]);
// When user starts connecting fields
const onConnectStart = useCallback(() => {
setIsConnectionInProgress(true);
}, []);
// When user ends connecting fields
const onConnectEnd = useCallback(() => {
setIsConnectionInProgress(false);
}, []);
// Automatically reposition tables to avoid overlap and fit the view
const adjustPositions = useCallback(async () => {
const adjustedTables = await adjustTablesPositions(nodes, relationships);
updateTablePositions(adjustedTables)
setTimeout(() => {
fitView({
duration: 500
});
}, 300);
}, [relationships, nodes]);
// Convert tables and relationships into flow elements
useTableToNode(tables);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
const isMobile = useIsMobile() ;
return (
<div className="w-full h-screen flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading ) && <Loading />
<Suspense
fallback={
<Loading/>
}
<div className="flex max-w-full">
{
<DBController />
}
</div>
>
{
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
animated: false
}}
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 !border-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="!bg-background border-1 rounded-lg overflow-hidden "
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
maskStrokeWidth={1}
nodeClassName={"!fill-muted-foreground/20 "}
style={{
width: 164,
height: 128
}}
/>
<Background color="#62748e" className="dark:!bg-background " />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="destructive"
size="icon"
className="shadow-lg size-8"
onClick={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive ">
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
</div>
isMobile ? <DatabaseMobileLayout/> : <DatabaseDesktopLayout/>
}
{
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
</defs>
</svg>
}
</div>
)
}
export default Database;
</Suspense>
);
};
export default DatabaseLayout;
+31
View File
@@ -0,0 +1,31 @@
import { Outlet } from "react-router-dom";
import DatabaseDiagram from "./components/database-diagram";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import {
Drawer,
DrawerContent,
} from "@/components/ui/drawer"
export const DatabaseMobileLayout: React.FC = () => {
const { showController, openController } = useDiagramOps()
return (
<div className="w-full h-full">
<DatabaseDiagram/>
<Drawer open={showController} onOpenChange={openController} >
<DrawerContent className=" min-h-[80vh] h-[80vh] bg-card">
<div className="mt-2 h-full overflow-hidden flex flex-col ">
<Outlet />
</div>
</DrawerContent>
</Drawer>
</div>
);
};
export default DatabaseMobileLayout;
@@ -88,7 +88,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
onOpenChange={onOpenChange}
title={t("db_controller.create_relationship")}
actionName={t("modals.create")}
className="min-w-[520px]"
className="md:min-w-[520px] w-full"
isDisabled={!isValid}
actionHandler={addRelationship}
>
@@ -26,7 +26,7 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
onOpenChange={onOpenChange}
title={t("modals.delete_database")}
actionName={t("modals.delete")}
className="min-w-[560px]"
className="md:min-w-[560px] w-full"
actionHandler={onDelete}
variant="danger"
>
@@ -15,13 +15,13 @@ const ExportSqlModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.export_sql")}
className="min-w-[960px] max-w-[960px] max-h-[90vh] "
className="w-full lg:min-w-[960px]"
description={t("modals.export_sql_header")}
>
<div className="h-full max-h-[72vh] ">
<div className="h-full max-h-[72vh] min-w-0">
<SqlPreview />
</div>
</Modal>
)
@@ -7,10 +7,10 @@ import { sql } from '@codemirror/lang-sql';
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 { AlertCircleIcon, Code } from "lucide-react";
import { AlertCircleIcon, Code, HelpCircle } from "lucide-react";
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
import Clipboard from "@/components/clipboard";
import { Trans } from 'react-i18next';
import { Trans } from 'react-i18next';
import { Node, useReactFlow } from "@xyflow/react";
import { adjustTablesPositions } from "@/utils/tables";
import { TableInsertType } from "@/lib/schemas/table-schema";
@@ -19,9 +19,12 @@ import { Label } from "@/components/ui/label";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { cn } from "@/lib/utils";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { buttonVariants } from "@/components/ui/button";
const options: ImportDatabaseOption[] = [{
dialect: DatabaseDialect.POSTGRES,
docUrl: "https://stackrender.io/docs/import/postgresql",
methods: [{
id: "pg_dump",
name: "pg_dump",
@@ -40,6 +43,7 @@ const options: ImportDatabaseOption[] = [{
}]
}, {
dialect: DatabaseDialect.MYSQL,
docUrl: "https://stackrender.io/docs/import/mysql",
methods: [{
id: "mysql_dump",
name: "mysqldump",
@@ -57,6 +61,7 @@ const options: ImportDatabaseOption[] = [{
}
, {
dialect: DatabaseDialect.MARIADB,
docUrl: "https://stackrender.io/docs/import/mariadb",
methods: [{
id: "mariadb-dump",
name: "mariadb-dump",
@@ -87,6 +92,7 @@ const options: ImportDatabaseOption[] = [{
}, {
dialect: DatabaseDialect.SQLITE,
docUrl: "https://stackrender.io/docs/import/sqlite",
methods: [{
id: "sqlite3",
name: "Sqlite3",
@@ -103,6 +109,8 @@ const options: ImportDatabaseOption[] = [{
}]
}
];
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { t } = useTranslation();
@@ -181,63 +189,74 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
}, [parsedDatabase]);
if (isLoading || isSwitchingDatabase)
return;
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.import_database.title")}
actionName={t("modals.import_database.import")}
className="!min-w-[860px] !max-w-[860px] flex flex-col"
className="lg:min-w-[860px] md:min-w-[560px] w-full max-h-screen overflow-auto "
actionHandler={onImport}
isDisabled={!parsedDatabase}
>
<div className="flex flex-col gap-4 ">
<div className="w-full space-y-2">
<div className="flex flex-col gap-4 !min-w-0 ">
<div className="w-full space-y-2 !min-w-0 ">
<p className="text-sm text-muted-foreground">
{t("modals.import_database.import_options")}
</p>
{
<RadioGroup
onValueChange={onImportMethodChange}
value={selectedMethodId}
className="flex ">
{
currentOption?.methods.map((method: ImportDatabaseMethod) => (
<Label htmlFor={method.id}
key={method.id}
className={cn(" h-8 px-2 rounded-md border hover:bg-secondary cursor-pointer flex items-center gap-1 ",
(selectedMethodId == method.id) ? "border-primary bg-primary/20 " : "",
)}>
<RadioGroupItem value={method.id} id={method.id} className="sr-only" />
{
method.icon &&
<div >
{method.icon}
</div>
}
<img
src={method.logo}
className="rounded-none h-4 "
/>
<span >
{method.name}
</span>
</Label>
))
<div className="flex flex-col lg:flex-row-reverse gap-2 justify-between items-center ">
<a
className={
buttonVariants({
variant: "outline"
})
}
</RadioGroup>
href={currentOption?.docUrl}
target="_blank"
>
{t("modals.import_database.view_docs")}
}
<HelpCircle className="size-4 text-muted-foreground" />
</a>
{
<RadioGroup
onValueChange={onImportMethodChange}
value={selectedMethodId}
className="flex ">
{
currentOption?.methods.map((method: ImportDatabaseMethod) => (
<Label htmlFor={method.id}
key={method.id}
className={cn(" h-8 px-2 rounded-md border hover:bg-secondary cursor-pointer flex items-center gap-1 ",
(selectedMethodId == method.id) ? "border-primary bg-primary/20 " : "",
)}>
<RadioGroupItem value={method.id} id={method.id} className="sr-only" />
{
method.icon &&
<div >
{method.icon}
</div>
}
<img
src={method.logo}
className="rounded-none h-4 "
/>
<span >
{method.name}
</span>
</Label>
))
}
</RadioGroup>
}
</div>
{
selectedImportMethod?.type == ImportMethodType.DUMP &&
<div className="space-y-2">
+2
View File
@@ -6,6 +6,7 @@ export const ar = {
tables: "الجداول",
relationships: "العلاقات",
database: "قاعدة البيانات",
documentation : "الوثائق"
},
color_picker: {
default_color: "اللون الافتراضي"
@@ -195,6 +196,7 @@ export const ar = {
import_database: {
title: "استيراد قاعدة البيانات",
import: "استيراد",
view_docs : "عرض الوثائق" ,
import_options: "هل ترغب في الاستيراد باستخدام:",
import_error: "خطأ في تحليل SQL",
import_error_description: "تعذر استيراد SQL الخاص بك بسبب وجود صيغة غير صحيحة.",
+2 -1
View File
@@ -6,7 +6,7 @@ export const de = {
tables: "Tabellen",
relationships: "Beziehungen",
database: "Datenbank",
documentation : "Dokumentation"
},
color_picker: {
default_color: "Standardfarbe"
@@ -196,6 +196,7 @@ export const de = {
import_database: {
title: "Datenbank importieren",
import: "Importieren",
view_docs : "Dokumentation anzeigen" ,
import_options: "Möchtest du importieren mit:",
import_error: "SQL-Parsing-Fehler",
import_error_description: "Wir konnten dein SQL nicht importieren, da es ungültige Syntax enthält.",
+11 -9
View File
@@ -6,8 +6,9 @@ export const en = {
translation: {
sidebar: {
tables: "Tables",
relationships: "Relationships",
database : "Database"
relationships: "Relationships",
database: "Database",
documentation : "Docs"
},
color_picker: {
@@ -15,9 +16,9 @@ export const en = {
},
navbar: {
rename_db: "Rename database",
search: "Search" ,
command : "Type a command or search..." ,
search: "Search",
command: "Type a command or search...",
},
db_controller: {
filter: "Filter",
@@ -43,7 +44,7 @@ export const en = {
source_table: "Source Table",
referenced_table: "Referenced Table",
required: "Required",
select_table: "Select table",
select_field: "Select field",
@@ -182,8 +183,8 @@ export const en = {
theme: "Theme",
light: "Light",
dark: "Dark",
system : "System" ,
system: "System",
help: "Help",
show_docs: "Show Docs",
join_discord: "Join Discord",
@@ -204,10 +205,11 @@ export const en = {
delete_database_content: "This action is irreversible and will permanently remove the diagram.",
delete: "Delete",
empty_diagram: "Empty diagram",
create_and_import : "Create and import" ,
create_and_import: "Create and import",
import_database: {
title: "Import your Database",
import: "Import",
view_docs : "View Docs" ,
import_options: "Would you like to import using :",
import_error: "SQL Parsing Error",
import_error_description: "We couldn't import your SQL because it contains invalid syntax.",
+3 -1
View File
@@ -5,7 +5,8 @@ export const es = {
translation: {
sidebar: {
tables: "Tablas",
relationships: "Relaciones"
relationships: "Relaciones",
documentation : "Documentación"
},
color_picker: {
@@ -204,6 +205,7 @@ export const es = {
import_database: {
title: "Importar tu base de datos",
import: "Importar",
view_docs : "Ver la documentación" ,
import_options: "¿Te gustaría importar usando :",
import_error: "Error al analizar SQL",
import_error_description: "No pudimos importar tu SQL porque contiene sintaxis inválida.",
+2
View File
@@ -7,6 +7,7 @@ export const fr = {
tables: "Tables",
relationships: "Relations",
database: "Base de données",
documentation : "Docs"
},
color_picker: {
@@ -198,6 +199,7 @@ export const fr = {
import_database: {
title: "Importer votre base de données",
import: "Importer",
view_docs : "Voir la documentation" ,
import_options: "Souhaitez-vous importer en utilisant :",
import_error: "Erreur danalyse SQL",
import_error_description: "Nous n'avons pas pu importer votre SQL car il contient une syntaxe invalide.",
+6 -6
View File
@@ -5,9 +5,8 @@ export const hi = {
sidebar: {
tables: "तालिकाएँ",
relationships: "रिलेशनशिप्स",
database: "डेटाबेस",
database: "डेटाबेस",
documentation : "दस्तावेज़"
},
color_picker: {
@@ -179,7 +178,7 @@ export const hi = {
theme: "थीम",
light: "हल्का",
dark: "गहरा",
system: "सिस्टम" ,
system: "सिस्टम",
help: "मदद",
show_docs: "दस्तावेज़ दिखाएं",
join_discord: "डिस्कॉर्ड से जुड़ें"
@@ -200,11 +199,12 @@ export const hi = {
delete_database_content: "यह क्रिया अपरिवर्तनीय है और आरेख को स्थायी रूप से हटा देगी।",
delete: "हटाएँ",
empty_diagram: "खाली आरेख",
create_and_import : "बनाएँ और आयात करें" ,
create_and_import: "बनाएँ और आयात करें",
import_database: {
title: "अपना डेटाबेस आयात करें",
import: "आयात करें",
view_docs : "दस्तावेज़ देखें" ,
import_options: "क्या आप निम्न के माध्यम से आयात करना चाहेंगे:",
import_error: "SQL पार्सिंग त्रुटि",
import_error_description: "हम आपका SQL आयात नहीं कर सके क्योंकि इसमें अमान्य सिंटैक्स है।",
@@ -216,7 +216,7 @@ export const hi = {
name: "नाम",
created_at: "बनाया गया",
tables: "टेबल्स"
} ,
},
export_sql: "SQL निर्यात करें",
export_sql_header: "अपने डेटाबेस आरेख को SQL कोड में निर्यात करें"
},
+2
View File
@@ -6,6 +6,7 @@ export const pt = {
tables: "Tabelas",
relationships: "Relacionamentos",
database: "Banco de dados",
documentation : "Documentação"
},
color_picker: {
@@ -204,6 +205,7 @@ export const pt = {
import_database: {
title: "Importar seu Banco de Dados",
import: "Importar",
view_docs : "Ver documentação" ,
import_options: "Gostaria de importar usando:",
import_error: "Erro de Análise SQL",
import_error_description: "Não foi possível importar seu SQL porque contém sintaxe inválida.",
+2 -1
View File
@@ -6,6 +6,7 @@ export const ru = {
tables: "Таблицы",
relationships: "Связи",
database: "База данных",
documentation : "Документация"
},
color_picker: {
default_color: "Цвет по умолчанию"
@@ -13,7 +14,6 @@ export const ru = {
navbar: {
rename_db: "Переименовать базу данных",
search: "Поиск",
command: "Введите команду или выполните поиск...",
},
db_controller: {
@@ -194,6 +194,7 @@ export const ru = {
import_database: {
title: "Импортируйте вашу базу данных",
import: "Импорт",
view_docs : "Просмотреть документацию" ,
import_options: "Вы хотите импортировать с помощью:",
import_error: "Ошибка разбора SQL",
import_error_description: "Не удалось импортировать SQL, так как он содержит недопустимый синтаксис.",
+2 -3
View File
@@ -6,8 +6,7 @@ export const zh = {
tables: "数据表",
relationships: "关系",
database: "数据库",
documentation: "文档"
},
color_picker: {
@@ -16,7 +15,6 @@ export const zh = {
navbar: {
rename_db: "重命名数据库",
search: "搜索",
command: "输入命令或搜索...",
},
db_controller: {
@@ -212,6 +210,7 @@ export const zh = {
import_database: {
title: "导入数据库",
import: "导入",
view_docs : "查看文档" ,
import_options: "请选择导入方式:",
import_error: "SQL 解析错误",
import_error_description: "我们无法导入您的 SQL,因为它包含无效语法。",
+2 -1
View File
@@ -88,7 +88,8 @@ export interface ImportDatabaseMethod {
export interface ImportDatabaseOption {
dialect: DatabaseDialect,
methods: ImportDatabaseMethod[]
methods: ImportDatabaseMethod[] ;
docUrl? : string ;
}
@@ -1,5 +1,5 @@
import { DatabaseDataContext, DatabaseOperationsContext } from "./database-context";
import { useCallback, useContext, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { db } from "../sync-provider/sync-provider";
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
import { useQuery } from "@powersync/react";
@@ -88,8 +88,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
else
database = undefined as any;
useEffect(() => {
console.log(database);
}, [database])
// Fetch all data types
let { data: data_types, isLoading: loadingDataTypes, isFetching: fetchingDatatypes } = useQuery(toCompilableQuery(
@@ -107,12 +110,12 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
})
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
const isSwitchingDatabase: boolean = useMemo(() => {
return currentDatabaseId != (database as any)?.id
}, [currentDatabaseId, database ]);
}, [currentDatabaseId, database]);
// CRUD for database
const createDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
@@ -226,7 +229,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
})
}, [db]);
// Reorder fields in a table
// Reorder fields in a table
const orderTables = useCallback(async (tablesList: TableType[]): Promise<void> => {
return await db.transaction(async (tx) => {
for (let index = 0; index < tablesList.length; index++) {
@@ -5,6 +5,9 @@ import { createContext, Dispatch } from "react";
interface DiagramDataContextType {
focusedTableId: string | undefined;
focusedRelationshipId: string | undefined;
setFocusedTableId : ( focused : string | undefined) => void ,
setFocusedRelationshipId : ( focused : string | undefined) => void
}
interface DiagramOpsContextType {
@@ -19,16 +19,16 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
const [focusedTableId, setFocusedTableId] = useState<string | undefined>(undefined)
const [focusedRelationshipId, setFocusedRelationshipId] = useState<string | undefined>(undefined)
const [isConnectionInProgress, setIsConnectionInProgress] = useState<boolean>(false);
const [showController, setShowController] = useState<boolean>(() => {
const value: string | null = localStorage.getItem(CONTROLLER_VISIBILITY_KEY) ;
const value: string | null = localStorage.getItem(CONTROLLER_VISIBILITY_KEY);
if (value)
return JSON.parse(value);
return true;
});
const [cardinalityStyle, setCardinalityStyle] = useState<CardinalityStyle>(() => {
const value: string | null = localStorage.getItem(CARDINALITY_STYLE_KEY);
if (value)
return value as CardinalityStyle;
return CardinalityStyle.SYMBOLIC;
@@ -101,19 +101,21 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
}, [setFocusedRelationshipId]);
const changeCardinalityStyle = useCallback((style : CardinalityStyle) => {
localStorage.setItem(CARDINALITY_STYLE_KEY , style) ;
setCardinalityStyle( style) ;
} , []) ;
const changeCardinalityStyle = useCallback((style: CardinalityStyle) => {
localStorage.setItem(CARDINALITY_STYLE_KEY, style);
setCardinalityStyle(style);
}, []);
const openController = useCallback((isOpen : boolean) => {
localStorage.setItem( CONTROLLER_VISIBILITY_KEY , JSON.stringify(isOpen) ) ;
setShowController(isOpen) ;
} , [])
const openController = useCallback((isOpen: boolean) => {
localStorage.setItem(CONTROLLER_VISIBILITY_KEY, JSON.stringify(isOpen));
setShowController(isOpen);
}, [])
const contextDatatValue = useMemo(() => ({
focusedTableId,
focusedRelationshipId,
setFocusedTableId,
setFocusedRelationshipId
}), [focusedTableId, focusedRelationshipId,]);
@@ -124,7 +126,7 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
isConnectionInProgress,
showController,
cardinalityStyle,
changeCardinalityStyle ,
changeCardinalityStyle,
openController
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle,
+2 -2
View File
@@ -1,5 +1,5 @@
import Database from '@/features/database/';
import DatabaseLayout from '@/features/database';
import RelationshipController from '@/features/database/components/db-controller/relationship-controller';
import TablesController from '@/features/database/components/db-controller/tables-controller';
import { Navigate, Route } from 'react-router-dom';
@@ -8,7 +8,7 @@ import { Navigate, Route } from 'react-router-dom';
const useDatabaseRoutes = () => {
return (
<Route path="/database" element={<Database />} >
<Route path="/database" element={<DatabaseLayout />} >
<Route index element={<Navigate to="tables" replace />} />
<Route path='tables' element={<TablesController />}></Route>
<Route path='relationships' element={<RelationshipController />}></Route>