Files
stackrender/src/features/database/components/db-controller/tables-controller/index.tsx
T
2026-06-10 21:43:46 +01:00

229 lines
8.1 KiB
TypeScript

import { Code } from "lucide-react"
import { useTranslation } from "react-i18next";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconListDetails, IconPlus } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
const PADDING_X = 40;
const PADDING_Y = 80;
import {
Accordion,
} from "@/components/ui/accordion"
import TableAccordionItem from "./table-accordion-item/table-accordion-item";
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 = ({ }) => {
const { database } = useDatabase();
const { createTable, getInteger, orderTables } = useDatabaseOperations();
const { getViewport } = useReactFlow();
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId , setFocusedTableId} = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const sensors = useSensors(
useSensor(PointerSensor)
);
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over?.id) {
setTables((items) => {
const oldIndex = items.findIndex((item: TableType) => item.id == active.id);
const newIndex = items.findIndex((item: TableType) => item.id == over.id);
const tables = arrayMove(items, oldIndex, newIndex);
orderTables(tables)
return tables;
});
}
}
useEffect(() => searchTables(), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
const viewport = getViewport();
const { x, y, zoom } = viewport;
const posX = -x / zoom + (PADDING_X / zoom);
const posY = -y / zoom + (PADDING_Y / zoom);
await createTable({
id: newTableId,
name: `table_${tables.length + 1}`,
posX,
posY,
sequence: getTableNextSequence(tables),
fields: [{
id: v4(),
name: "id",
isPrimary: true,
typeId: getInteger()?.id,
autoIncrement: true,
}]
} as TableInsertType);
setFocusedTableId(newTableId);
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: "nearest"
})
}
}, [focusedTableId]);
const searchTables = useCallback(() => {
const keyword = nameRef.current?.value;
if (keyword !== undefined)
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
else
setTables(allTables)
}, [nameRef, allTables]);
const toggleSqlPreview = useCallback(() => {
setShowSqlPreview(preview => !preview);
}, []);
const tableFilterIds = useMemo(() => {
return tables.map((table: TableType) => table.id);
}, [tables]);
return (
<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
variant={"ghost"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={toggleSqlPreview}
>
{showSqlPreview ? (
<IconListDetails className="size-4" stroke={1} />
) : (
<Code className="size-4 text-muted-foreground" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.show_code")}
</TooltipContent>
</Tooltip>
<Input
className="h-8 bg-secondary dark:bg-background"
placeholder={t("db_controller.filter")}
ref={nameRef}
type="text"
onKeyUp={searchTables}
/>
<Tooltip >
<TooltipTrigger asChild>
<Button
variant={"default"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={addNewTable}
>
<IconPlus className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.add_table")}
</TooltipContent>
</Tooltip>
</div>
{
allTables.length > 0 && !showSqlPreview &&
<ScrollArea className="px-3 h-full w-full overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full"
value={focusedTableId}
onValueChange={setFocusedTableId}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={tables}
strategy={verticalListSortingStrategy}
>
{tables.map((table: TableType) => (
<>
<TableAccordionItem table={table} key={table.id} />
<Separator className="my-1" />
</>
))}
</SortableContext>
</DndContext>
</Accordion>
</ScrollArea>
}
{
allTables.length > 0 && showSqlPreview &&
<div className=" flex-1 overflow-auto ">
<SqlPreview
tableFilterIds={tableFilterIds}
className="rounded-none border-0 border-t-1"
/>
</div>
}
{
(allTables.length == 0) &&
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_tables")}
description={t("db_controller.empty_list.no_tables_description")}
/>
</div>
}
</div>
)
}
export default TablesController