mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
Merge remote-tracking branch 'origin/main' into copilot/dockerize-the-application
This commit is contained in:
+1
-1
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Generated
+24
-37
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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")}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+11
-4
@@ -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")}
|
||||
|
||||
+14
-5
@@ -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}
|
||||
>
|
||||
|
||||
+4
-4
@@ -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}
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+11
-4
@@ -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"
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 الخاص بك بسبب وجود صيغة غير صحيحة.",
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 d’analyse SQL",
|
||||
import_error_description: "Nous n'avons pas pu importer votre SQL car il contient une syntaxe invalide.",
|
||||
|
||||
@@ -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 कोड में निर्यात करें"
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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, так как он содержит недопустимый синтаксис.",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user