Improve performance and accessibility; add documentation

This commit is contained in:
KarimTamani
2026-03-28 19:22:59 +01:00
parent 39317143b1
commit aa3688a0e6
25 changed files with 173 additions and 116 deletions
@@ -14,6 +14,7 @@ import { IconFilter2Up, IconPlus } from "@tabler/icons-react";
import { Accordion } from "@/components/ui/accordion";
import RelationshipAccordionItem from "./relationship-accordion-item/relationship-accordion-item";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
const RelationshipController: React.FC = ({ }) => {
@@ -22,32 +23,28 @@ const RelationshipController: React.FC = ({ }) => {
const { database } = useDatabase();
const { relationships: allRelationships } = database || { relationships: [] };
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [selectedRelationship, setSelectedRelationship] = useState<string | undefined>();
const { focusedRelationshipId } = useDiagram();
const { t } = useTranslation();
const { focusedRelationshipId , setFocusedRelationshipId } = useDiagram();
useEffect(() => searchRelationships(), [allRelationships]);
useEffect(() => {
if (focusedRelationshipId) {
setSelectedRelationship(focusedRelationshipId);
if (focusedRelationshipId) {
const accordionItem = document.getElementById(focusedRelationshipId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
behavior: 'smooth', block: "nearest"
})
}
}, [focusedRelationshipId]);
const onOpen = useCallback(() => {
open(Modals.CREATE_RELATIONSHIP, {
onRlationshipCreated: (id: string) => setSelectedRelationship(id)
onRlationshipCreated: (id: string) => setFocusedRelationshipId(id)
})
}, []);
const searchRelationships = useCallback(() => {
const keyword: string | undefined = nameRef.current?.value;
if (keyword !== undefined) {
@@ -66,13 +63,12 @@ const RelationshipController: React.FC = ({ }) => {
}, [nameRef, allRelationships]);
const collapseAll = useCallback(() => {
setSelectedRelationship("");
setFocusedRelationshipId(undefined);
}, []);
return (
<div className="w-full h-full flex flex-col pl-3 min-h-0">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
@@ -116,16 +112,19 @@ const RelationshipController: React.FC = ({ }) => {
{
allRelationships.length > 0 &&
<ScrollArea className=" flex-1 overflow-auto pr-3">
<ScrollArea className="flex-1 px-3 overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full "
value={selectedRelationship}
onValueChange={setSelectedRelationship}
value={focusedRelationshipId}
onValueChange={setFocusedRelationshipId}
>
{relationships.map((relationship: RelationshipType) => (
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<>
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<Separator className="my-1" />
</>
))}
</Accordion>
</ScrollArea>
@@ -133,7 +132,7 @@ const RelationshipController: React.FC = ({ }) => {
{
(allRelationships.length == 0) &&
<div className="pr-2 h-full">
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_relationships")}
description={t("db_controller.empty_list.no_relationships_description")}
@@ -15,7 +15,7 @@ interface RelationshipAccordionItemProps {
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
return (
<AccordionItem value={relationship.id} className="my-2" id={relationship.id}>
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
<RelationshipAccordionTrigger relationship={relationship} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<RelationshipAccordionContent
@@ -66,13 +66,13 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
</label>
</div>
<div className="hidden shrink-0 flex-row group-hover:flex gap-2">
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnRelationship(relationship.id, true)
}}>
@@ -92,16 +92,23 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
editRelationshipName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" onClick={editRelationshipName}>
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={editRelationshipName}>
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" >
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
@@ -9,8 +9,7 @@ import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
import EmptyList from "@/components/empty-list";
import { useModal } from "@/providers/modal-provider/modal-provider";
import EmptyList from "@/components/empty-list";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconListDetails, IconPlus } from "@tabler/icons-react";
@@ -28,6 +27,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { getTableNextSequence } from "@/utils/tables";
import { Separator } from "@/components/ui/separator";
const TablesController: React.FC = ({ }) => {
@@ -38,9 +38,9 @@ const TablesController: React.FC = ({ }) => {
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState<string | undefined>(undefined);
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId } = useDiagram();
const { focusedTableId , setFocusedTableId} = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const sensors = useSensors(
@@ -59,7 +59,7 @@ const TablesController: React.FC = ({ }) => {
const newIndex = items.findIndex((item: TableType) => item.id == over.id);
const tables = arrayMove(items, oldIndex, newIndex);
orderTables(tables)
return tables;
@@ -72,7 +72,7 @@ const TablesController: React.FC = ({ }) => {
useEffect(() => searchTables(), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
const viewport = getViewport();
const { x, y, zoom } = viewport;
@@ -96,17 +96,15 @@ const TablesController: React.FC = ({ }) => {
}]
} as TableInsertType);
setSelectedTable(newTableId);
setFocusedTableId(newTableId);
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
setSelectedTable(focusedTableId);
setShowSqlPreview(false);
if (focusedTableId) {
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
behavior: 'smooth', block: "nearest"
})
}
}, [focusedTableId]);
@@ -128,8 +126,8 @@ const TablesController: React.FC = ({ }) => {
}, [tables]);
return (
<div className="w-full h-full flex flex-col pl-3 min-h-0">
<div className="flex items-center pb-2 gap-2 pt-3 pr-3 ">
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
@@ -174,13 +172,13 @@ const TablesController: React.FC = ({ }) => {
</div>
{
allTables.length > 0 && !showSqlPreview &&
<ScrollArea className="overflow-y-auto pr-3 h-full w-full ">
<ScrollArea className="px-3 h-full w-full overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full"
value={selectedTable}
onValueChange={setSelectedTable}
value={focusedTableId}
onValueChange={setFocusedTableId}
>
<DndContext
sensors={sensors}
@@ -194,7 +192,10 @@ const TablesController: React.FC = ({ }) => {
>
{tables.map((table: TableType) => (
<TableAccordionItem table={table} key={table.id} />
<>
<TableAccordionItem table={table} key={table.id} />
<Separator className="my-1" />
</>
))}
</SortableContext>
@@ -213,7 +214,7 @@ const TablesController: React.FC = ({ }) => {
}
{
(allTables.length == 0) &&
<div className="pr-3 h-full">
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_tables")}
description={t("db_controller.empty_list.no_tables_description")}
@@ -4,7 +4,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldType } from "@/lib/schemas/field-schema";
import { FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import FieldSetting from "./field-setting";
@@ -61,6 +61,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
if (!dataType)
return;
if (dataType.id == field.typeId)
return;
if (key != null) {
editField({
id: field.id,
@@ -98,6 +100,13 @@ const FieldItem: React.FC<Props> = ({ field }) => {
onChange={(event: any) => setFieldName(event.target.value)}
onBlur={saveFieldName}
className=" flex flex-1 !bg-transparent"
onKeyDown={(e : any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName() ;
e.target.blur() ;
}
}}
/>
<Combobox
items={data_types}
@@ -113,9 +122,9 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground "
pressed={!field.nullable as boolean}
className="size-9 text-muted-foreground "
pressed={!field.nullable as boolean}
onPressedChange={toggleNullable}
>
<IconKeyframe className="size-4" />
@@ -130,8 +139,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground "
pressed={field.isPrimary as boolean}
className="size-9 text-muted-foreground "
pressed={field.isPrimary as boolean}
onPressedChange={togglePrimaryKey}
>
<IconKey className="size-4" />
@@ -91,7 +91,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
>
<AccordionItem value="fields" className="border-none">
<AccordionTrigger className="py-1 rounded-none text-muted-foreground " position="right" >
<AccordionTrigger className="py-1 rounded-none text-muted-foreground pr-1.5" position="right" >
<div className="w-full flex gap-1 items-center">
<IconFolder className="size-4" />
{t("db_controller.fields")}
@@ -103,7 +103,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
<AccordionItem value="indexes" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground " position="right">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconBolt className="size-4" />
{t("db_controller.indexes")}
@@ -115,7 +115,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
<AccordionItem value="note" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground " position="right">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconMessageCircle className="size-4 " />
{t("db_controller.note")}
@@ -133,7 +133,7 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</AccordionItem>
</Accordion>
<div className="flex pt-2 justify-between items-center ">
<div className="flex pt-2 justify-between pr-1.5 items-center ">
<div className="h-full flex items-center">
<ColorPicker
defaultColor={table.color as string}
@@ -28,12 +28,12 @@ const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
return (
<AccordionItem value={table.id} className="my-2 border-none" id={table.id} ref={ref}>
<AccordionItem value={table.id} className=" border-none" id={table.id} ref={ref}>
<div
ref={setNodeRef}
style={style}
{...attributes}
className="w-full border-b "
className="w-full "
>
<TableAccordionTrigger table={table} />
<AccordionContent className="flex flex-col gap-4 text-balance">
@@ -126,14 +126,14 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
</Tooltip>
</div>
<div className="hidden !shrink-0 flex-row group-hover:flex gap-2 ">
<Button variant="outline" size="icon" className="size-7 shrink-0" onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0" onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-7 shrink-0 " onClick={(event: any) => {
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnTable(table.id, true)
}}>
@@ -154,9 +154,16 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-7 shrink-0 rounded-sm" onClick={saveTableName} >
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={saveTableName} >
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
@@ -164,7 +171,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-7 shrink-0 flex rounded-sm" >
<Button variant="ghost" size="icon" className="size-8 shrink-0 flex rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
@@ -98,6 +98,13 @@ const Field: React.FC<Props> = (props) => {
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="h-6 font-bold pb-1.5 rounded-sm px-1"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName();
e.target.blur();
}
}}
/>
}
+12 -5
View File
@@ -4,7 +4,7 @@ import { Edge, Node, NodeProps } from "@xyflow/react";
import hash from 'object-hash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Check,
Check,
ChevronUp,
ChevronDown,
} from 'lucide-react';
@@ -15,14 +15,12 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
import { useTranslation } from "react-i18next";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Card, CardContent , CardHeader } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react";
import { IconFocus2, IconPencil, IconTableFilled } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export type TableProps = Node<{
table: TableType,
overlapping?: boolean,
@@ -102,6 +100,8 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
}
: undefined
}
onDoubleClick={focus}
>
<CardHeader className="group rounded-t rounded-t-md p-1.5 flex items-center mb-0 bg-primary/10 "
@@ -142,6 +142,13 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
value={tableName}
onBlur={saveTableName}
style={{ color: table.color as string }}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
e.target.blur();
}
}}
/>
<div className="flex gap-1 shrink-0 flex-row transition-opacity duration-200">
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
@@ -7,10 +7,10 @@ import { sql } from '@codemirror/lang-sql';
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType, MARIADB_DUMP_EXAMPLE, MARIADB_DUMP_INSTRUCTIONS, MYSQL_DUMP_EXAMPLE, MYSQL_DUMP_INSTRUCTIONS, PG_DUMP_EXAMPLE, PG_DUMP_INSTRUCTIONS, SQLITE_DUMP_EXAMPLE, SQLITE_DUMP_INSRUCTION } from "@/lib/database";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { AlertCircleIcon, Code } from "lucide-react";
import { AlertCircleIcon, Code } from "lucide-react";
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
import Clipboard from "@/components/clipboard";
import { Trans } from 'react-i18next';
import { Trans } from 'react-i18next';
import { Node, useReactFlow } from "@xyflow/react";
import { adjustTablesPositions } from "@/utils/tables";
import { TableInsertType } from "@/lib/schemas/table-schema";
@@ -181,20 +181,16 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
}, [parsedDatabase]);
if (isLoading || isSwitchingDatabase)
return;
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.import_database.title")}
actionName={t("modals.import_database.import")}
className="lg:min-w-[860px] md:min-w-[560px] w-full max-h-screen overflow-auto "
className="lg:min-w-[860px] md:min-w-[560px] w-full max-h-screen overflow-auto "
actionHandler={onImport}
isDisabled={!parsedDatabase}
>