Mobile Responsivity

This commit is contained in:
KarimTamani
2026-01-01 15:49:48 +01:00
parent 7c443f0edf
commit d9e5333f96
24 changed files with 728 additions and 491 deletions
+28 -41
View File
@@ -1,12 +1,12 @@
{
"name": "vite-template",
"version": "0.0.0",
"name": "StackRender",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vite-template",
"version": "0.0.0",
"name": "StackRender",
"version": "1.0.1",
"dependencies": {
"@codemirror/lang-sql": "^6.9.0",
"@dnd-kit/core": "^6.3.1",
@@ -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",
+3 -4
View File
@@ -41,7 +41,6 @@
"@tabler/icons-react": "^3.34.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
"@types/react-resizable": "^3.0.8",
"@uiw/react-codemirror": "^4.23.12",
"@xyflow/react": "^12.6.3",
"class-variance-authority": "^0.7.1",
@@ -64,7 +63,7 @@
"react-day-picker": "^9.10.0",
"react-dom": "18.3.1",
"react-i18next": "^15.5.1",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "6.23.0",
"react-tag-input": "^6.10.6",
"sonner": "^2.0.7",
@@ -74,7 +73,8 @@
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",
"use-undo": "^1.1.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
@@ -83,7 +83,6 @@
"@types/pluralize": "^0.0.33",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-resizable": "^3.0.8",
"@typescript-eslint/eslint-plugin": "8.11.0",
"@typescript-eslint/parser": "8.11.0",
"@vitejs/plugin-react": "^4.2.1",
+6 -3
View File
@@ -64,9 +64,12 @@ export const Combobox = (props: ComboboxProps) => {
aria-expanded={open}
className={cn("justify-between", className)}
>
{value
? items.find((item) => item.id === value)?.[label]
: (placeholder ? placeholder : "Select items...")}
<label className="truncate">
{
value
? items.find((item) => item.id === value)?.[label]
: (placeholder ? placeholder : "Select items...")}
</label>
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
+22 -12
View File
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'
import { ThemeSwitch } from './theme-switch'
import RenameDb from '@/features/database/components/rename-db'
import LanguagesDropdown from './languages-dropdown'
import { useIsMobile } from '@/hooks/use-mobile'
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
fixed?: boolean
@@ -38,19 +39,13 @@ export const Header = ({
return () => document.removeEventListener('scroll', onScroll)
}, []);
const { open } = useModal();
const { t } = useTranslation();
const isMobile = useIsMobile();
return (
<header
className={cn(
'bg-background flex items-center gap-3 p-2 sm:gap-4 ',
'bg-background flex-col md:flex items-center gap-3 p-2 sm:gap-4 ',
fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md ',
offset > 10 && fixed ? 'shadow-sm' : 'shadow-none',
className
@@ -61,23 +56,38 @@ export const Header = ({
<div className="justify-self-start flex h-8 gap-3 items-center ">
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
<Separator orientation='vertical' className='h-6' />
<Menu />
{!isMobile && <Menu />}
</div>
<div className=" justify-self-center ">
<Search className='hidden md:flex' placeholder={t("navbar.search")} />
<Search className='hidden lg:flex' placeholder={t("navbar.search")} />
{
isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
</div>
<div className="justify-self-end flex gap-2">
<RenameDb />
{
!isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
<ThemeSwitch />
<LanguagesDropdown />
</div>
</div>
{isMobile && <div className='flex justify-center'>
<Menu />
</div>
}
</header>
)
}
@@ -10,9 +10,11 @@ import {
import { NavGroup } from '../nav-group'
import { useSidebarData } from './data/sidebar-data'
import { Separator } from '@/components/ui/separator';
import { useDiagramOps } from '@/providers/diagram-provider/diagram-provider';
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const sidebarData = useSidebarData();
const { openController } = useDiagramOps();
return (
<Sidebar collapsible='icon' variant='floating' {...props} className='bg-card border-r' >
<SidebarHeader>
@@ -24,12 +26,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<h3 className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground truncate font-semibold text-sm '>
StackRender
</h3>
</div>
</div>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((props: any, index: number) => (
<div key={props.title}>
<NavGroup key={props.title} {...props} />
<NavGroup key={props.title} {...props} onClick={() => openController(true)} />
{
index != sidebarData.navGroups.length - 1 &&
<div className='px-2'>
@@ -50,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</div>
}
</div>
))}
))}
</SidebarFooter>
<SidebarRail />
</Sidebar>
+4 -3
View File
@@ -29,7 +29,7 @@ import {
} from '../ui/dropdown-menu'
import { NavCollapsible, NavItem, NavLink, NavGroup as NavGroupType } from './types.ts'
export function NavGroup({ title, items }: NavGroupType) {
export function NavGroup({ title, items , onClick }: NavGroupType) {
const { state, isMobile } = useSidebar();
const location = useLocation();
const href = location.pathname
@@ -41,7 +41,7 @@ export function NavGroup({ title, items }: NavGroupType) {
const key = `${item.title}-${item.url}`
if (!item.items)
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined}/>
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined} onClick={onClick}/>
if (state === 'collapsed' && !isMobile)
return (
@@ -59,7 +59,7 @@ const NavBadge = ({ children }: { children: ReactNode }) => (
<Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
)
const SidebarMenuLink = ({ item, href , target}: { item: NavLink; href: string; target?: string }) => {
const SidebarMenuLink = ({ item, href , target , onClick}: { item: NavLink; href: string; target?: string , onClick? : ()=> void }) => {
const { setOpenMobile } = useSidebar()
return (
<SidebarMenuItem>
@@ -67,6 +67,7 @@ const SidebarMenuLink = ({ item, href , target}: { item: NavLink; href: string;
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
onClick={onClick}
>
<Link to={item.url} onClick={() => setOpenMobile(false)} target={target}>
{item.icon && <item.icon />}
+8 -7
View File
@@ -1,5 +1,5 @@
interface BaseNavItem {
@@ -10,8 +10,8 @@ interface BaseNavItem {
type NavLink = BaseNavItem & {
url: string
items?: never;
newTab? : boolean
items?: never;
newTab?: boolean
}
type NavCollapsible = BaseNavItem & {
@@ -23,13 +23,14 @@ type NavItem = NavCollapsible | NavLink
interface NavGroup {
title: string
items: NavItem[]
items: NavItem[];
onClick?: () => void;
}
interface SidebarData {
footerNavGroups : NavGroup[]
footerNavGroups: NavGroup[]
navGroups: NavGroup[]
}
@@ -47,4 +48,4 @@ interface MenuItem {
clickHandler?: () => void
}
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink , MenuItem }
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink, MenuItem }
+1 -1
View File
@@ -67,7 +67,7 @@ const Modal: React.FC<ModalProps> = ({
{
footer &&
<DialogFooter >
<div className="flex justify-between w-full">
<div className="flex justify-between w-full flex-col gap-4 sm:flex-row">
{
+133
View File
@@ -0,0 +1,133 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
+56
View File
@@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
@@ -0,0 +1,351 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
} from "@xyflow/react";
// React built-ins
import { useCallback, useEffect, useMemo } from "react";
// Custom components and styles
import Table from "./table";
import '@xyflow/react/dist/style.css';
import Relationship from "./relationship";
import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
import { TARGET_PREFIX } from "./field";
import CardinalityMarker from "@/features/database/components/cardinality-marker";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { AlertTriangle, Menu } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import { Loading } from "@/components/layout/loading-modal";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import DatabaseControlButtons from "./database-control-buttons";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const DatabaseDiagram: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { theme: resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField, isSwitchingDatabase, isLoading, databases } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
// Node and edge state hooks
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
// Hook to allow zooming and centering the diagram
const { fitView } = useReactFlow();
// Define custom node and edge types
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
useEffect(() => {
if (isLoading)
return;
// no database selected , open (open database) Modal
if (!database && databases?.length > 0) {
open(Modals.OPEN_DATABASE, {
closable: false
})
}
else if (databases.length == 0) {
// there is no databases in the first place to select from ,
// we have to create a new one .
open(Modals.CREATE_DATABASE, {
closable: false
});
} else {
fitView({
duration: 500
});
}
}, [database?.id, isLoading])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase, database?.id])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
await createRelationship({
id: v4(),
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
} else {
toast(t("db_controller.invalid_relationship.title"), {
description: t("db_controller.invalid_relationship.description"),
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
})
}
setIsConnectionInProgress(false);
}, [database, t]);
// Called when nodes are updated (position changes or removed)
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
change.type == "position" && !change.dragging
) as NodePositionChange[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange, database, isSwitchingDatabase]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange, isSwitchingDatabase]);
// When user starts connecting fields
const onConnectStart = useCallback(() => {
setIsConnectionInProgress(true);
}, []);
// When user ends connecting fields
const onConnectEnd = useCallback(() => {
setIsConnectionInProgress(false);
}, []);
// Automatically reposition tables to avoid overlap and fit the view
const adjustPositions = useCallback(async () => {
const adjustedTables = await adjustTablesPositions(nodes, relationships);
updateTablePositions(adjustedTables)
setTimeout(() => {
fitView({
duration: 500
});
}, 300);
}, [relationships, nodes]);
// Convert tables and relationships into flow elements
useTableToNode(tables);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
const isMobile = useIsMobile();
return (
<div className="w-full h-full flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading) && <Loading />
}
{
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
animated: false
}}
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
minZoom={0.10}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position={isMobile ? "top-center" : "bottom-center"}
showFitView={false}
showZoom={false}
showInteractive={false}
className="!shadow-none !border-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="!bg-background border-1 rounded-lg overflow-hidden "
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
maskStrokeWidth={1}
nodeClassName={"!fill-muted-foreground/20 "}
style={{
width: 164,
height: 128
}}
/>
<Background color="#62748e" className="dark:!bg-background " />
</ReactFlow>
<div
className={cn("absolute left-[24px] ", {
"bottom-[24px]": !isMobile,
"top-[24px]": isMobile
})}
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="destructive"
size="icon"
className="shadow-lg size-8"
onClick={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive ">
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
{
isMobile &&
<Button size={"icon"} className="absolute bottom-[24px] left-[24px]"
onClick={() => {
openController(!showController)
}}
>
<Menu className="size-5" />
</Button>
}
</div>
}
{
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
</defs>
</svg>
}
</div>
)
}
export default DatabaseDiagram;
@@ -1,68 +0,0 @@
import React, { useEffect, useState } from "react"
import { Outlet } from "react-router-dom"
import { ResizableBox as ResizableBoxRaw } from 'react-resizable';
import 'react-resizable/css/styles.css';
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
interface Props {
}
const DBController: React.FC<Props> = ({ }) => {
const [width, setWidth] = useState<number>(512);
const { showController } = useDiagramOps()
const [show, setShow] = useState<boolean>(!showController);
const [style, setStyle] = useState<any>(undefined);
const onResize = (event: any, params: any) => {
setWidth(params.size.width);
}
useEffect(() => {
if (!showController) {
setWidth(0);
setStyle({
transition: 'width 0.5s',
})
setTimeout(() => {
setShow(false);
}, 500);
} else if (showController) {
setShow(true);
setTimeout(() => setWidth(486));
setTimeout(() => {
setStyle(undefined);
}, 500)
}
}, [showController])
if (show)
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full overflow-hidden "
minConstraints={[486]}
axis="x"
style={style}
handle={
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary/50 active:border-primary transition-colors duration-200 ">
</div>
}
>
<div className="min-w-[486px] w-full h-full bg-background dark:bg-card border-r ">
<Outlet />
</div>
</ResizableBox>
)
else
return;
}
export default React.memo(DBController)
@@ -70,7 +70,7 @@ const RelationshipController: React.FC = ({ }) => {
}, []);
return (
<div className="w-full h-full flex flex-col pl-3 ">
<div className="w-full h-full flex flex-col pl-3 min-h-0">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<Tooltip >
@@ -128,7 +128,7 @@ const TablesController: React.FC = ({ }) => {
}, [tables]);
return (
<div className="w-full h-full flex flex-col pl-3 ">
<div className="w-full h-full flex flex-col pl-3 min-h-0">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<Tooltip >
<TooltipTrigger asChild>
@@ -105,7 +105,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,9 +113,9 @@ 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"
pressed={!field.nullable as boolean}
className="size-9 text-muted-foreground "
pressed={!field.nullable as boolean}
onPressedChange={toggleNullable}
>
<IconKeyframe className="size-4" />
@@ -130,8 +130,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground data-[state=on]:border-1"
pressed={field.isPrimary as boolean}
className="size-9 text-muted-foreground "
pressed={field.isPrimary as boolean}
onPressedChange={togglePrimaryKey}
>
<IconKey className="size-4" />
@@ -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"
+37
View File
@@ -0,0 +1,37 @@
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { cn } from "@/lib/utils";
import { Outlet } from "react-router-dom";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import DatabaseDiagram from "./components/database-diagram";
export const DatabaseDesktopLayout: React.FC = () => {
const { showController } = useDiagramOps()
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
minSize={25}
maxSize={showController ? 99 : 0}
className={cn('transition-[flex-grow] duration-500 bg-card ', {
"min-w-[460px]": showController
})}
>
<div className="min-w-[460px] h-full">
<Outlet />
</div>
</ResizablePanel>
<ResizableHandle
disabled={!showController}
className={cn("h-full cursor-ew-resize hover:bg-primary/50 active:border-primary transition-colors duration-200", !showController ? 'hidden' : '')}
/>
<ResizablePanel defaultSize={75} >
<DatabaseDiagram/>
</ResizablePanel>
</ResizablePanelGroup>
);
};
export default DatabaseDesktopLayout;
+22 -328
View File
@@ -1,340 +1,34 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
} from "@xyflow/react";
// React built-ins
import { useCallback, useEffect, useMemo } from "react";
// Custom components and styles
import Table from "./components/table";
import '@xyflow/react/dist/style.css';
import Relationship from "./components/relationship";
import DBController from "./components/db-controller";
import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
import { TARGET_PREFIX } from "./components/field";
import CardinalityMarker from "@/features/database/components/cardinality-marker";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { AlertTriangle } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import React, { Suspense } from "react";
import { Loading } from "@/components/layout/loading-modal";
import { usePowerSync } from "@powersync/react";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import DatabaseControlButtons from "./components/database-control-buttons";
import RenameDb from "./components/rename-db";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useIsMobile } from "@/hooks/use-mobile";
const DatabaseDesktopLayout = React.lazy(
() => import('./desktop-layout')
);
const Database: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { theme: resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField, isSwitchingDatabase, isLoading, databases } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
// Node and edge state hooks
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
// Hook to allow zooming and centering the diagram
const { fitView } = useReactFlow();
// Define custom node and edge types
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
const DatabaseMobileLayout = React.lazy(
() => import('./mobile-layout')
);
export const DatabaseLayout: React.FC = () => {
useEffect(() => {
if (isLoading)
return;
// no database selected , open (open database) Modal
if (!database && databases?.length > 0) {
open(Modals.OPEN_DATABASE, {
closable: false
})
}
else if (databases.length == 0) {
// there is no databases in the first place to select from ,
// we have to create a new one .
open(Modals.CREATE_DATABASE, {
closable: false
});
} else {
fitView({
duration: 500
});
}
}, [database?.id, isLoading])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase, database?.id])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
await createRelationship({
id: v4(),
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
} else {
toast(t("db_controller.invalid_relationship.title"), {
description: t("db_controller.invalid_relationship.description"),
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
})
}
setIsConnectionInProgress(false);
}, [database, t]);
// Called when nodes are updated (position changes or removed)
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
change.type == "position" && !change.dragging
) as NodePositionChange[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange, database, isSwitchingDatabase]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange, isSwitchingDatabase]);
// When user starts connecting fields
const onConnectStart = useCallback(() => {
setIsConnectionInProgress(true);
}, []);
// When user ends connecting fields
const onConnectEnd = useCallback(() => {
setIsConnectionInProgress(false);
}, []);
// Automatically reposition tables to avoid overlap and fit the view
const adjustPositions = useCallback(async () => {
const adjustedTables = await adjustTablesPositions(nodes, relationships);
updateTablePositions(adjustedTables)
setTimeout(() => {
fitView({
duration: 500
});
}, 300);
}, [relationships, nodes]);
// Convert tables and relationships into flow elements
useTableToNode(tables);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
const isMobile = useIsMobile() ;
return (
<div className="w-full h-screen flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading ) && <Loading />
<Suspense
fallback={
<Loading/>
}
<div className="flex max-w-full">
{
<DBController />
}
</div>
>
{
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
animated: false
}}
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
minZoom={0.10}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="!shadow-none !border-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="!bg-background border-1 rounded-lg overflow-hidden "
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
maskStrokeWidth={1}
nodeClassName={"!fill-muted-foreground/20 "}
style={{
width: 164,
height: 128
}}
/>
<Background color="#62748e" className="dark:!bg-background " />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="destructive"
size="icon"
className="shadow-lg size-8"
onClick={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive ">
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
</div>
isMobile ? <DatabaseMobileLayout/> : <DatabaseDesktopLayout/>
}
{
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
</defs>
</svg>
}
</div>
)
}
export default Database;
</Suspense>
);
};
export default DatabaseLayout;
+31
View File
@@ -0,0 +1,31 @@
import { Outlet } from "react-router-dom";
import DatabaseDiagram from "./components/database-diagram";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import {
Drawer,
DrawerContent,
} from "@/components/ui/drawer"
export const DatabaseMobileLayout: React.FC = () => {
const { showController, openController } = useDiagramOps()
return (
<div className="w-full h-full">
<DatabaseDiagram/>
<Drawer open={showController} onOpenChange={openController} >
<DrawerContent className=" min-h-[80vh] h-[80vh] bg-card">
<div className="mt-2 h-full overflow-hidden flex flex-col ">
<Outlet />
</div>
</DrawerContent>
</Drawer>
</div>
);
};
export default DatabaseMobileLayout;
@@ -88,7 +88,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
onOpenChange={onOpenChange}
title={t("db_controller.create_relationship")}
actionName={t("modals.create")}
className="min-w-[520px]"
className="md:min-w-[520px] w-full"
isDisabled={!isValid}
actionHandler={addRelationship}
>
@@ -26,7 +26,7 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
onOpenChange={onOpenChange}
title={t("modals.delete_database")}
actionName={t("modals.delete")}
className="min-w-[560px]"
className="md:min-w-[560px] w-full"
actionHandler={onDelete}
variant="danger"
>
@@ -15,13 +15,13 @@ const ExportSqlModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.export_sql")}
className="min-w-[960px] max-w-[960px] max-h-[90vh] "
className="w-full lg:min-w-[960px]"
description={t("modals.export_sql_header")}
>
<div className="h-full max-h-[72vh] ">
<div className="h-full max-h-[72vh] min-w-0">
<SqlPreview />
</div>
</Modal>
)
@@ -194,12 +194,12 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
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>
+2 -2
View File
@@ -1,5 +1,5 @@
import Database from '@/features/database/';
import DatabaseLayout from '@/features/database';
import RelationshipController from '@/features/database/components/db-controller/relationship-controller';
import TablesController from '@/features/database/components/db-controller/tables-controller';
import { Navigate, Route } from 'react-router-dom';
@@ -8,7 +8,7 @@ import { Navigate, Route } from 'react-router-dom';
const useDatabaseRoutes = () => {
return (
<Route path="/database" element={<Database />} >
<Route path="/database" element={<DatabaseLayout />} >
<Route index element={<Navigate to="tables" replace />} />
<Route path='tables' element={<TablesController />}></Route>
<Route path='relationships' element={<RelationshipController />}></Route>