diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 448d650..64d984d 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3184cc0..18321ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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). diff --git a/README.md b/README.md index 6b667e4..2e20c9d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Start Building! - + Discord community channel @@ -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. diff --git a/SECURITY.md b/SECURITY.md index 44a1c1f..9dc8792 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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: diff --git a/package-lock.json b/package-lock.json index 5d335b2..fc27720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5d8fae0..57fe958 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/combobox.tsx b/src/components/combobox.tsx index d694c8a..1e5ed05 100644 --- a/src/components/combobox.tsx +++ b/src/components/combobox.tsx @@ -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...")} + diff --git a/src/components/header.tsx b/src/components/header.tsx index b655ffa..145a617 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -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 { fixed?: boolean @@ -38,19 +39,13 @@ export const Header = ({ return () => document.removeEventListener('scroll', onScroll) }, []); - - - - const { open } = useModal(); - - const { t } = useTranslation(); - + const isMobile = useIsMobile(); return (
10 && fixed ? 'shadow-sm' : 'shadow-none', className @@ -61,23 +56,38 @@ export const Header = ({
- + {!isMobile && }
- + + { + isMobile && +
+ +
+ }
- - + { + !isMobile && +
+ +
+ } +
+ {isMobile &&
+ +
+ }
) } diff --git a/src/components/layout/app-sidebar/app-sidebar.tsx b/src/components/layout/app-sidebar/app-sidebar.tsx index 8653402..dc4e0ae 100644 --- a/src/components/layout/app-sidebar/app-sidebar.tsx +++ b/src/components/layout/app-sidebar/app-sidebar.tsx @@ -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) { const sidebarData = useSidebarData(); + const { openController } = useDiagramOps(); return ( @@ -24,12 +26,12 @@ export function AppSidebar({ ...props }: React.ComponentProps) {

StackRender

- +
{sidebarData.navGroups.map((props: any, index: number) => (
- + openController(true)} /> { index != sidebarData.navGroups.length - 1 &&
@@ -50,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
}
- ))} + ))}
diff --git a/src/components/layout/app-sidebar/data/sidebar-data.tsx b/src/components/layout/app-sidebar/data/sidebar-data.tsx index 1dd087e..c13ca4e 100644 --- a/src/components/layout/app-sidebar/data/sidebar-data.tsx +++ b/src/components/layout/app-sidebar/data/sidebar-data.tsx @@ -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 + } ], }] diff --git a/src/components/layout/menu/data/menu-data.tsx b/src/components/layout/menu/data/menu-data.tsx index 6504682..ee1cb29 100644 --- a/src/components/layout/menu/data/menu-data.tsx +++ b/src/components/layout/menu/data/menu-data.tsx @@ -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") } }, ], diff --git a/src/components/layout/nav-group.tsx b/src/components/layout/nav-group.tsx index 29fe56d..a5a0503 100644 --- a/src/components/layout/nav-group.tsx +++ b/src/components/layout/nav-group.tsx @@ -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 + return if (state === 'collapsed' && !isMobile) return ( @@ -59,7 +59,7 @@ const NavBadge = ({ children }: { children: ReactNode }) => ( {children} ) -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 ( @@ -67,6 +67,7 @@ const SidebarMenuLink = ({ item, href , target}: { item: NavLink; href: string; asChild isActive={checkIsActive(href, item)} tooltip={item.title} + onClick={onClick} > setOpenMobile(false)} target={target}> {item.icon && } diff --git a/src/components/layout/types.ts b/src/components/layout/types.ts index 8a2624e..8266997 100644 --- a/src/components/layout/types.ts +++ b/src/components/layout/types.ts @@ -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 } diff --git a/src/components/modal.tsx b/src/components/modal.tsx index f7aa491..b57fb36 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -67,7 +67,7 @@ const Modal: React.FC = ({ { footer && -
+
{ diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..869955f --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -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) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx new file mode 100644 index 0000000..12bbd0b --- /dev/null +++ b/src/components/ui/resizable.tsx @@ -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) { + return ( + + ) +} + +function ResizablePanel({ + ...props +}: React.ComponentProps) { + return +} + +function ResizableHandle({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) { + return ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+ ) +} + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/src/features/database/components/database-diagram.tsx b/src/features/database/components/database-diagram.tsx new file mode 100644 index 0000000..5bd504d --- /dev/null +++ b/src/features/database/components/database-diagram.tsx @@ -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 = useCallback(async (changes: NodeChange[]) => { + + 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 = useCallback((changes: EdgeChange[]) => { + 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[]); + }, [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 ( + +
+ { + (isSwitchingDatabase || isLoading) && + } + + { +
+ + + + + + + + +
+ { + isOverlapping && + + + + + + + + {t("table.overlapping_tables")} + + + } +
+ { + isMobile && + + } +
+ + } + { + (cardinalityStyle != CardinalityStyle.HIDDEN) && + + + + + + + + + + + + + + + + + } +
+ + ) +} + + +export default DatabaseDiagram; + + + + + diff --git a/src/features/database/components/db-controller/index.tsx b/src/features/database/components/db-controller/index.tsx deleted file mode 100644 index 77334fe..0000000 --- a/src/features/database/components/db-controller/index.tsx +++ /dev/null @@ -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; - -interface Props { - -} - - -const DBController: React.FC = ({ }) => { - const [width, setWidth] = useState(512); - const { showController } = useDiagramOps() - const [show, setShow] = useState(!showController); - const [style, setStyle] = useState(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 ( - -
- } - > -
- -
- - ) - else - return; -} - -export default React.memo(DBController) - - diff --git a/src/features/database/components/db-controller/relationship-controller/index.tsx b/src/features/database/components/db-controller/relationship-controller/index.tsx index f3dcf73..d567f8f 100644 --- a/src/features/database/components/db-controller/relationship-controller/index.tsx +++ b/src/features/database/components/db-controller/relationship-controller/index.tsx @@ -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(allRelationships); - const nameRef: Ref = useRef(null); - const { t } = useTranslation(); - const [selectedRelationship, setSelectedRelationship] = useState(); - 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 ( -
- -
+
+
- } - diff --git a/src/features/database/components/db-controller/tables-controller/index.tsx b/src/features/database/components/db-controller/tables-controller/index.tsx index c61b532..b51c02a 100644 --- a/src/features/database/components/db-controller/tables-controller/index.tsx +++ b/src/features/database/components/db-controller/tables-controller/index.tsx @@ -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(allTables); const { t } = useTranslation(); - const [selectedTable, setSelectedTable] = useState(undefined); + const [showSqlPreview, setShowSqlPreview] = useState(false); - const { focusedTableId } = useDiagram(); + const { focusedTableId , setFocusedTableId} = useDiagram(); const nameRef: Ref = useRef(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 ( -
-
+
+
{ allTables.length > 0 && !showSqlPreview && - + { > {tables.map((table: TableType) => ( - + <> + + + ))} @@ -213,7 +214,7 @@ const TablesController: React.FC = ({ }) => { } { (allTables.length == 0) && -
+
= ({ 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 = ({ 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() ; + } + }} /> = ({ 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" />
@@ -113,8 +122,8 @@ const FieldItem: React.FC = ({ field }) => { @@ -130,7 +139,7 @@ const FieldItem: React.FC = ({ field }) => { diff --git a/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-content.tsx b/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-content.tsx index 724cd48..a411ae0 100644 --- a/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-content.tsx +++ b/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-content.tsx @@ -91,7 +91,7 @@ const TableAccordionContent: React.FC = ({ table, keys > - +
{t("db_controller.fields")} @@ -103,7 +103,7 @@ const TableAccordionContent: React.FC = ({ table, keys - +
{t("db_controller.indexes")} @@ -115,7 +115,7 @@ const TableAccordionContent: React.FC = ({ table, keys - +
{t("db_controller.note")} @@ -133,7 +133,7 @@ const TableAccordionContent: React.FC = ({ table, keys -
+
( return ( - +
diff --git a/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-trigger.tsx b/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-trigger.tsx index 59e9806..4464aec 100644 --- a/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-trigger.tsx +++ b/src/features/database/components/db-controller/tables-controller/table-accordion-item/table-accordion-trigger.tsx @@ -126,14 +126,14 @@ const TableAccordionHeader: React.FC = ({ table, isOp
- - @@ -164,7 +171,7 @@ const TableAccordionHeader: React.FC = ({ table, isOp - diff --git a/src/features/database/components/field.tsx b/src/features/database/components/field.tsx index 15e94f5..cd973b0 100644 --- a/src/features/database/components/field.tsx +++ b/src/features/database/components/field.tsx @@ -98,6 +98,13 @@ const Field: React.FC = (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(); + } + }} /> } diff --git a/src/features/database/components/rename-db.tsx b/src/features/database/components/rename-db.tsx index e9db97a..30d39a4 100644 --- a/src/features/database/components/rename-db.tsx +++ b/src/features/database/components/rename-db.tsx @@ -46,7 +46,7 @@ const RenameDB: React.FC = ({ }) => { }); setIsLoading(false); setEditMode(false); - } + } if (!database) @@ -59,7 +59,7 @@ const RenameDB: React.FC = ({ }) => { - - {t("table.overlapping_tables")} - - - } -
-
- + isMobile ? : } - { - (cardinalityStyle != CardinalityStyle.HIDDEN) && - - - - - - - - - - - - - - - - - } -
- - ) -} - - -export default Database; - - - - + + + ); +}; +export default DatabaseLayout; diff --git a/src/features/database/mobile-layout.tsx b/src/features/database/mobile-layout.tsx new file mode 100644 index 0000000..db32a34 --- /dev/null +++ b/src/features/database/mobile-layout.tsx @@ -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 ( +
+ + + + +
+ +
+
+
+
+ ); +}; + +export default DatabaseMobileLayout; diff --git a/src/features/database/modals/create-relationship-modal.tsx b/src/features/database/modals/create-relationship-modal.tsx index aa86f8c..560e7b2 100644 --- a/src/features/database/modals/create-relationship-modal.tsx +++ b/src/features/database/modals/create-relationship-modal.tsx @@ -88,7 +88,7 @@ const CreateRelationshipModal: React.FC = ({ 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} > diff --git a/src/features/database/modals/delete-database-modal.tsx b/src/features/database/modals/delete-database-modal.tsx index 500c751..ba4118b 100644 --- a/src/features/database/modals/delete-database-modal.tsx +++ b/src/features/database/modals/delete-database-modal.tsx @@ -26,7 +26,7 @@ const DeleteDatabaseModal: React.FC = ({ 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" > diff --git a/src/features/database/modals/export-sql-modal.tsx b/src/features/database/modals/export-sql-modal.tsx index cb5d836..769bc0e 100644 --- a/src/features/database/modals/export-sql-modal.tsx +++ b/src/features/database/modals/export-sql-modal.tsx @@ -15,13 +15,13 @@ const ExportSqlModal: React.FC = ({ 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")} > -
+
- + ) diff --git a/src/features/database/modals/import-database.tsx b/src/features/database/modals/import-database.tsx index 22b2691..e07d773 100644 --- a/src/features/database/modals/import-database.tsx +++ b/src/features/database/modals/import-database.tsx @@ -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 = ({ isOpen, onOpenChange }) => { const { t } = useTranslation(); @@ -181,63 +189,74 @@ const ImportDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => }, [parsedDatabase]); - - if (isLoading || isSwitchingDatabase) return; - - return ( -
-
+
+

{t("modals.import_database.import_options")}

- { - - { - currentOption?.methods.map((method: ImportDatabaseMethod) => ( - - )) +
+ + href={currentOption?.docUrl} + target="_blank" + > + {t("modals.import_database.view_docs")} - } + + + { + + { + currentOption?.methods.map((method: ImportDatabaseMethod) => ( + + )) + } + + + } +
{ selectedImportMethod?.type == ImportMethodType.DUMP &&
diff --git a/src/i18/languages/ar.ts b/src/i18/languages/ar.ts index 70e8c21..681190c 100644 --- a/src/i18/languages/ar.ts +++ b/src/i18/languages/ar.ts @@ -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 الخاص بك بسبب وجود صيغة غير صحيحة.", diff --git a/src/i18/languages/de.ts b/src/i18/languages/de.ts index 0cae024..f7a86ea 100644 --- a/src/i18/languages/de.ts +++ b/src/i18/languages/de.ts @@ -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.", diff --git a/src/i18/languages/en.ts b/src/i18/languages/en.ts index b58fc62..6e1e00f 100644 --- a/src/i18/languages/en.ts +++ b/src/i18/languages/en.ts @@ -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.", diff --git a/src/i18/languages/es.ts b/src/i18/languages/es.ts index a7f0e4c..aab2a18 100644 --- a/src/i18/languages/es.ts +++ b/src/i18/languages/es.ts @@ -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.", diff --git a/src/i18/languages/fr.ts b/src/i18/languages/fr.ts index 4f71079..94831e8 100644 --- a/src/i18/languages/fr.ts +++ b/src/i18/languages/fr.ts @@ -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.", diff --git a/src/i18/languages/hi.ts b/src/i18/languages/hi.ts index f110821..35c0372 100644 --- a/src/i18/languages/hi.ts +++ b/src/i18/languages/hi.ts @@ -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 कोड में निर्यात करें" }, diff --git a/src/i18/languages/pt.ts b/src/i18/languages/pt.ts index 2a19f1e..385314c 100644 --- a/src/i18/languages/pt.ts +++ b/src/i18/languages/pt.ts @@ -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.", diff --git a/src/i18/languages/ru.ts b/src/i18/languages/ru.ts index ef9e9ff..690497d 100644 --- a/src/i18/languages/ru.ts +++ b/src/i18/languages/ru.ts @@ -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, так как он содержит недопустимый синтаксис.", diff --git a/src/i18/languages/zh.ts b/src/i18/languages/zh.ts index 8613fc1..902edbd 100644 --- a/src/i18/languages/zh.ts +++ b/src/i18/languages/zh.ts @@ -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,因为它包含无效语法。", diff --git a/src/lib/database.ts b/src/lib/database.ts index 95b9028..a2a70ed 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -88,7 +88,8 @@ export interface ImportDatabaseMethod { export interface ImportDatabaseOption { dialect: DatabaseDialect, - methods: ImportDatabaseMethod[] + methods: ImportDatabaseMethod[] ; + docUrl? : string ; } diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 2f0eb9c..4d2e4d7 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -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 = ({ 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 = ({ 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 => { @@ -226,7 +229,7 @@ const DatabaseProvider: React.FC = ({ children }) => { }) }, [db]); - // Reorder fields in a table + // Reorder fields in a table const orderTables = useCallback(async (tablesList: TableType[]): Promise => { return await db.transaction(async (tx) => { for (let index = 0; index < tablesList.length; index++) { diff --git a/src/providers/diagram-provider/diagram-context.tsx b/src/providers/diagram-provider/diagram-context.tsx index 4d536e8..506ae01 100644 --- a/src/providers/diagram-provider/diagram-context.tsx +++ b/src/providers/diagram-provider/diagram-context.tsx @@ -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 { diff --git a/src/providers/diagram-provider/diagram-provider.tsx b/src/providers/diagram-provider/diagram-provider.tsx index 9bef846..a1f1cdc 100644 --- a/src/providers/diagram-provider/diagram-provider.tsx +++ b/src/providers/diagram-provider/diagram-provider.tsx @@ -19,16 +19,16 @@ const DiagramProvider: React.FC = ({ children }) => { const [focusedTableId, setFocusedTableId] = useState(undefined) const [focusedRelationshipId, setFocusedRelationshipId] = useState(undefined) const [isConnectionInProgress, setIsConnectionInProgress] = useState(false); - + const [showController, setShowController] = useState(() => { - 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(() => { 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 = ({ 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 = ({ children }) => { isConnectionInProgress, showController, cardinalityStyle, - changeCardinalityStyle , + changeCardinalityStyle, openController }), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle, diff --git a/src/routes/database-route.tsx b/src/routes/database-route.tsx index f3c9323..479e6e3 100644 --- a/src/routes/database-route.tsx +++ b/src/routes/database-route.tsx @@ -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 ( - } > + } > } /> }> }>