From d9e5333f96542fab3ad64d86ceb14d8f98067cfe Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Thu, 1 Jan 2026 15:49:48 +0100 Subject: [PATCH 1/6] Mobile Responsivity --- package-lock.json | 69 ++-- package.json | 7 +- src/components/combobox.tsx | 9 +- src/components/header.tsx | 34 +- .../layout/app-sidebar/app-sidebar.tsx | 8 +- src/components/layout/nav-group.tsx | 7 +- src/components/layout/types.ts | 15 +- src/components/modal.tsx | 2 +- src/components/ui/drawer.tsx | 133 +++++++ src/components/ui/resizable.tsx | 56 +++ .../database/components/database-diagram.tsx | 351 ++++++++++++++++++ .../components/db-controller/index.tsx | 68 ---- .../relationship-controller/index.tsx | 2 +- .../db-controller/tables-controller/index.tsx | 2 +- .../table-accordion-item/field/field-item.tsx | 12 +- .../database/components/rename-db.tsx | 6 +- src/features/database/desktop-layout.tsx | 37 ++ src/features/database/index.tsx | 350 ++--------------- src/features/database/mobile-layout.tsx | 31 ++ .../modals/create-relationship-modal.tsx | 2 +- .../database/modals/delete-database-modal.tsx | 2 +- .../database/modals/export-sql-modal.tsx | 6 +- .../database/modals/import-database.tsx | 6 +- src/routes/database-route.tsx | 4 +- 24 files changed, 728 insertions(+), 491 deletions(-) create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/resizable.tsx create mode 100644 src/features/database/components/database-diagram.tsx delete mode 100644 src/features/database/components/db-controller/index.tsx create mode 100644 src/features/database/desktop-layout.tsx create mode 100644 src/features/database/mobile-layout.tsx diff --git a/package-lock.json b/package-lock.json index 7fe6a99..fc27720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5d8fae0..7e6c388 100644 --- a/package.json +++ b/package.json @@ -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/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..4b17191 100644 --- a/src/features/database/components/db-controller/relationship-controller/index.tsx +++ b/src/features/database/components/db-controller/relationship-controller/index.tsx @@ -70,7 +70,7 @@ const RelationshipController: React.FC = ({ }) => { }, []); 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..e6f29d9 100644 --- a/src/features/database/components/db-controller/tables-controller/index.tsx +++ b/src/features/database/components/db-controller/tables-controller/index.tsx @@ -128,7 +128,7 @@ const TablesController: React.FC = ({ }) => { }, [tables]); return ( -
+
diff --git a/src/features/database/components/db-controller/tables-controller/table-accordion-item/field/field-item.tsx b/src/features/database/components/db-controller/tables-controller/table-accordion-item/field/field-item.tsx index 8aac485..32b1b4e 100644 --- a/src/features/database/components/db-controller/tables-controller/table-accordion-item/field/field-item.tsx +++ b/src/features/database/components/db-controller/tables-controller/table-accordion-item/field/field-item.tsx @@ -105,7 +105,7 @@ const FieldItem: React.FC = ({ 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,9 +113,9 @@ const FieldItem: React.FC = ({ field }) => { @@ -130,8 +130,8 @@ const FieldItem: React.FC = ({ field }) => { 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..e51b736 100644 --- a/src/features/database/modals/import-database.tsx +++ b/src/features/database/modals/import-database.tsx @@ -194,12 +194,12 @@ const ImportDatabaseModal: React.FC = ({ 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} > -
-
+
+

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

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 ( - } > + } > } /> }> }> From 1d78d054689799345975b233170f9bb52e37834a Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Thu, 1 Jan 2026 15:51:17 +0100 Subject: [PATCH 2/6] V1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e6c388..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", From 39317143b10769bec257fc5fc16203acfb5bd299 Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Mon, 9 Feb 2026 22:11:29 +0100 Subject: [PATCH 3/6] Update Discord link & Security contaact email --- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- SECURITY.md | 2 +- src/components/layout/app-sidebar/data/sidebar-data.tsx | 2 +- src/components/layout/menu/data/menu-data.tsx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) 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 a1e5b43..380e6c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Start Building! - + Discord community channel @@ -68,7 +68,7 @@ npm run start ## 🤝 Community -- [**Discord**](https://discord.com/invite/DsN8RcPR6Y) – Live discussions about upcoming releases and the future of StackRender. +- [**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. @@ -76,7 +76,7 @@ npm run start 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/src/components/layout/app-sidebar/data/sidebar-data.tsx b/src/components/layout/app-sidebar/data/sidebar-data.tsx index 1dd087e..741c0d2 100644 --- a/src/components/layout/app-sidebar/data/sidebar-data.tsx +++ b/src/components/layout/app-sidebar/data/sidebar-data.tsx @@ -46,7 +46,7 @@ export const useSidebarData = () => { }, { title: "Discord", - url: "https://discord.gg/DsN8RcPR6Y", + url: "https://discord.gg/4dv26jR4Pj", icon: IconBrandDiscord, newTab : true }, diff --git a/src/components/layout/menu/data/menu-data.tsx b/src/components/layout/menu/data/menu-data.tsx index 6504682..8d8c234 100644 --- a/src/components/layout/menu/data/menu-data.tsx +++ b/src/components/layout/menu/data/menu-data.tsx @@ -153,7 +153,7 @@ export const useMenuData = () => { { id: "menu.join_discord", title: t("menu.join_discord"), clickHandler: () => { - window.open("https://discord.gg/DsN8RcPR6Y", "_blank") + window.open("https://discord.gg/4dv26jR4Pj", "_blank") } }, ], From aa3688a0e6bed35eb5773dc7080f49ca52edd6bf Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Sat, 28 Mar 2026 19:22:59 +0100 Subject: [PATCH 4/6] Improve performance and accessibility; add documentation --- .../layout/app-sidebar/data/sidebar-data.tsx | 15 ++++---- src/components/layout/menu/data/menu-data.tsx | 8 +++- .../relationship-controller/index.tsx | 35 +++++++++--------- .../relationship-accordion-item.tsx | 2 +- .../relationship-accordion-trigger.tsx | 15 ++++++-- .../db-controller/tables-controller/index.tsx | 37 ++++++++++--------- .../table-accordion-item/field/field-item.tsx | 21 ++++++++--- .../table-accordion-content.tsx | 8 ++-- .../table-accordion-item.tsx | 4 +- .../table-accordion-trigger.tsx | 15 ++++++-- src/features/database/components/field.tsx | 7 ++++ src/features/database/components/table.tsx | 17 ++++++--- .../database/modals/import-database.tsx | 10 ++--- src/i18/languages/ar.ts | 2 + src/i18/languages/de.ts | 3 +- src/i18/languages/en.ts | 20 +++++----- src/i18/languages/es.ts | 4 +- src/i18/languages/fr.ts | 2 + src/i18/languages/hi.ts | 12 +++--- src/i18/languages/pt.ts | 2 + src/i18/languages/ru.ts | 3 +- src/i18/languages/zh.ts | 5 +-- .../database-provider/database-provider.tsx | 13 ++++--- .../diagram-provider/diagram-context.tsx | 3 ++ .../diagram-provider/diagram-provider.tsx | 26 +++++++------ 25 files changed, 173 insertions(+), 116 deletions(-) diff --git a/src/components/layout/app-sidebar/data/sidebar-data.tsx b/src/components/layout/app-sidebar/data/sidebar-data.tsx index 741c0d2..3a474c2 100644 --- a/src/components/layout/app-sidebar/data/sidebar-data.tsx +++ b/src/components/layout/app-sidebar/data/sidebar-data.tsx @@ -10,6 +10,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,13 +38,7 @@ 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/4dv26jR4Pj", @@ -56,6 +51,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 8d8c234..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,6 +148,12 @@ 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: () => { 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 4b17191..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 e6f29d9..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 }) => { @@ -130,8 +139,8 @@ 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/table.tsx b/src/features/database/components/table.tsx index 4ee41c1..9d3ed35 100644 --- a/src/features/database/components/table.tsx +++ b/src/features/database/components/table.tsx @@ -4,7 +4,7 @@ import { Edge, Node, NodeProps } from "@xyflow/react"; import hash from 'object-hash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { - Check, + Check, ChevronUp, ChevronDown, } from 'lucide-react'; @@ -15,14 +15,12 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr import { useTranslation } from "react-i18next"; import { RelationshipType } from "@/lib/schemas/relationship-schema"; import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider"; -import { Card, CardContent , CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react"; +import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; - - export type TableProps = Node<{ table: TableType, overlapping?: boolean, @@ -102,6 +100,8 @@ const Table: React.FC> = (props) => { } : undefined } + onDoubleClick={focus} + > > = (props) => { value={tableName} onBlur={saveTableName} style={{ color: table.color as string }} + onKeyDown={(e: any) => { + if (e.key === "Enter") { + e.preventDefault(); + saveTableName(); + e.target.blur(); + } + }} />