search for table & relationships functionalities implemented

This commit is contained in:
KarimTamani
2025-05-26 18:19:50 +01:00
parent 6d41e73af8
commit ed13965f61
12 changed files with 174 additions and 51 deletions
+9 -3
View File
@@ -6,11 +6,17 @@ import { useTranslation } from "react-i18next";
export const useRelationshipName = (relationship: RelationshipType) => {
const { t } = useTranslation();
const name: string = useMemo(() => {
if ( !relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
return "" ;
return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk`
return getDefaultRelationshipName(relationship) ;
}, [relationship, t])
return {
name
}
}
export const getDefaultRelationshipName = (relationship: RelationshipType) => {
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
return "";
return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk`
}
+6 -2
View File
@@ -20,6 +20,8 @@ export const en = {
add_table: "Add Table",
add_field: "Add Field",
add_index: "Add Index",
delete_table : "Delete Table" ,
duplicate : "Duplicate" ,
add_relationship: "Add Relationship",
show_code: "Show code",
fields: "Fields",
@@ -55,7 +57,7 @@ export const en = {
},
delete: "Delete",
field_setting: "Field Setting",
table_actions: "Field Actions",
table_actions: "Table Actions",
actions: "Actions",
field_note: "Field note",
@@ -71,7 +73,9 @@ export const en = {
},
table: {
double_click: "Double click to edit" ,
overlapping_tables : "Overlapping Tables"
overlapping_tables : "Overlapping Tables" ,
show_more : "Show more" ,
show_less : "Show less"
},
control_buttons: {
redo: "Redo",
+1 -1
View File
@@ -157,7 +157,7 @@ const DatabasePage: React.FC<never> = () => {
return (
<div className="w-full h-screen flex relative overflow-hidden">
<div>
<div className="flex max-w-full">
<DBController />
</div>
<div className="relative w-full h-full">
@@ -21,7 +21,7 @@ const DBController: React.FC<Props> = ({ }) => {
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full"
onResize={onResize} className="min-h-full "
minConstraints={[512]}
axis="x"
handle={
@@ -43,14 +43,6 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
}, [relationship.cardinality])
useEffect(() => {
console.log("re-render relationship controlelr", name);
}, [relationship])
if (!relationship.sourceTable || !relationship.targetTable)
return;
@@ -103,7 +95,7 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
className="w-full"
size="sm"
variant="bordered"
label="cardinality"
aria-label="cardinality"
selectedKeys={cardinality}
onSelectionChange={changeCardinality}
@@ -1,7 +1,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
import { Accordion, AccordionItem, Button, Input, useDisclosure } from "@heroui/react"
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
import { useCallback, useEffect, useMemo, useState } from "react";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
@@ -11,6 +11,7 @@ import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relation
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { getDefaultRelationshipName } from "@/hooks/use-relationship-name";
@@ -25,13 +26,17 @@ const RelationshipController: React.FC<Props> = ({ }) => {
const [isValid, setIsValid] = useState<boolean>(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { database } = useDatabase();
const { createRelationship } = useDatabaseOperations();
const { relationships } = database;
const { createRelationship } = useDatabaseOperations();
const { relationships: allRelationships } = database;
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
const { focusedRelationshipId } = useDiagram();
useEffect(() => setRelationships(allRelationships), [allRelationships]);
const addRelationship = useCallback(() => {
const newRelationshipId: string = v4();
@@ -50,7 +55,28 @@ const RelationshipController: React.FC<Props> = ({ }) => {
setSelectedRelationship(new Set([focusedRelationshipId]) as any);
}
}, [focusedRelationshipId]) ;
}, [focusedRelationshipId]);
const searchRelationships = useCallback(() => {
const keyword: string | undefined = nameRef.current?.value;
if (keyword !== undefined) {
setRelationships(() => {
return allRelationships.filter((relationship: RelationshipType) => {
if (relationship.name)
return relationship.name.toLowerCase().trim().includes(keyword.toLocaleLowerCase().trim());
else
return getDefaultRelationshipName(relationship).trim().toLocaleLowerCase().includes(
keyword.trim().toLocaleLowerCase()
)
})
})
}
}, [nameRef, allRelationships]);
const selectedRelationshipId = selectedRelationship.values().next().value;
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -80,17 +106,16 @@ const RelationshipController: React.FC<Props> = ({ }) => {
</div>
<div className="flex-1">
<Input
//ref={filterInputRef}
ref={nameRef}
type="text"
size="sm"
radius="sm"
variant="bordered"
placeholder={t("db_controller.filter")}
//placeholder={t('side_panel.tables_section.filter')}
className="h-8 w-full focus-visible:ring-0 "
//value={filterText}
//onChange={(e) => setFilterText(e.target.value)}
className="h-8 w-full focus-visible:ring-0"
onKeyUp={searchRelationships}
/>
</div>
<Button
@@ -126,7 +151,9 @@ const RelationshipController: React.FC<Props> = ({ }) => {
base: "rounded-md p-0 overflow-hidden",
}}
subtitle={
<RelationshipAccordionHeader relationship={relationship} />
<RelationshipAccordionHeader
isOpen={selectedRelationshipId == relationship.id}
relationship={relationship} />
}
>
<RelationshipAccordionBody relationship={relationship} />
@@ -11,6 +11,7 @@ import { v4 } from "uuid";
import { getNextSequence } from "@/utils/field";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import hash from "object-hash";
import { cloneTable } from "@/utils/tables";
export interface TableAccordionHeaderProps {
table: TableType,
@@ -19,7 +20,8 @@ export interface TableAccordionHeaderProps {
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const { editTable, deleteTable, createField } = useDatabaseOperations();
const { editTable, deleteTable, createField, createTable } = useDatabaseOperations();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
@@ -54,8 +56,15 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
}
const duplicate = async () => {
setPopOverOpen(false)
const clonedTable: TableType = cloneTable(table);
await createTable(clonedTable)
focusOnTable(clonedTable.id);
}
return (
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center border-l-[6px] border-default"
@@ -162,20 +171,23 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<FileType className="size-4 text-icon dark:text-white" />
}>
Add Field
{t("db_controller.add_field")}
</ListboxItem>
<ListboxItem
key="add_index"
endContent={<FileKey className="size-4 text-icon dark:text-white" />}
showDivider>
Add Index
{t("db_controller.add_index")}
</ListboxItem>
<ListboxItem
key="duplicate"
showDivider
onPressEnd={duplicate}
endContent={<Copy className="size-4 text-icon" />}>
Duplicate
{t("db_controller.duplicate")}
</ListboxItem>
<ListboxItem
key="delete"
@@ -183,8 +195,8 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
color="danger"
onPressEnd={onDeleteTable}
endContent={<Trash className="size-4" />}
>
Delete Table
>
{t("db_controller.delete_table")}
</ListboxItem>
</Listbox>
</PopoverContent>
@@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
import { ChevronDown, Code, EllipsisVertical, Focus, Grid, Grip, List, Pencil, Table } from "lucide-react"
import { useTranslation } from "react-i18next";
import { useCallback, useEffect, useState } from "react";
import { Ref, useCallback, useEffect, useRef, useState } from "react";
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
@@ -21,11 +21,16 @@ const TablesController: React.FC<Props> = ({ }) => {
const { database } = useDatabase();
const { createTable } = useDatabaseOperations();
const { getViewport } = useReactFlow();
const { tables } = database;
//const viewport = useViewport();
const { tables: allTables } = database;
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState(new Set([]));
const { focusedTableId } = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
useEffect(() => setTables(allTables), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
@@ -59,11 +64,12 @@ const TablesController: React.FC<Props> = ({ }) => {
}, [focusedTableId]);
const selectedTableId = selectedTable.values().next().value;
useEffect(() => {
console.log("tables controller re-rendered")
}, [getViewport]);
const searchTables = useCallback(() => {
const keyword = nameRef.current?.value;
if (keyword !== undefined)
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
}, [nameRef, allTables])
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -96,15 +102,15 @@ const TablesController: React.FC<Props> = ({ }) => {
</div>
<div className="flex-1">
<Input
//ref={filterInputRef}
ref={nameRef}
type="text"
size="sm"
autoFocus
radius="sm"
variant="bordered"
placeholder={t("db_controller.filter")}
className="h-8 w-full focus-visible:ring-0"
//value={filterText}
//onChange={(e) => setFilterText(e.target.value)}
onKeyUp={searchTables}
/>
</div>
<Button
+17 -2
View File
@@ -4,10 +4,11 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Button, cn } from "@heroui/react";
import { Handle, Position } from "@xyflow/react";
import { Check, KeyRound, Trash2 } from "lucide-react";
import { Check, KeyRound, MessageSquareQuote, Trash2 } from "lucide-react";
import React, { useCallback, useEffect, useState } from "react";
import hash from 'object-hash';
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
interface Props {
field: FieldType,
@@ -57,13 +58,27 @@ const Field: React.FC<Props> = (props) => {
<>
<label
className={cn(
"truncate text-xs text-slate-900 dark:text-default-200",
"truncate flex gap-1 text-xs text-slate-900 dark:text-default-200",
field.isPrimary ? "font-semibold" : ""
)}
onDoubleClick={() => setEditMode(true)}
>
{fieldName}
{
field.note && !editMode &&
<Tooltip>
<TooltipTrigger>
<MessageSquareQuote className="size-3.5 text-icon" />
</TooltipTrigger>
<TooltipContent >
{field.note}
</TooltipContent>
</Tooltip>
}
</label>
<span className={cn("content-center truncate flex items-center h-full gap-1 text-right text-xs text-default-600 group-hover:hidden font-semibold text-icon dark:text-default-200",
field.isPrimary ? "font-semibold text-slate-700" : ""
)}>
+33 -4
View File
@@ -8,6 +8,8 @@ import {
Table2,
Check,
Focus,
ChevronUp,
ChevronDown,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
@@ -29,12 +31,14 @@ export type TableProps = Node<{
}>
const MAX_FIELDS = 10;
const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [] } }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
const { editTable } = useDatabaseOperations();
const [showMore, setShowMore] = useState<boolean>(false);
const { focusOnTable } = useDiagramOps();
const { t } = useTranslation();
@@ -69,6 +73,11 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
})
}, [table.fields, selected, highlightedEdges]);
const toggleShowMore = useCallback(() => {
setShowMore((previousShowMore) => !previousShowMore);
}, [])
return (
<Card className={cn(
"w-full h-full bg-background rounded-lg noselect overflow-visible dark:bg-default-900",
@@ -78,7 +87,7 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
overlapping
? 'ring-2 dark:ring-offset-default-900 ring-danger ring-offset-1 scale-105 shadow-danger '
: '',
!pulsing && overlapping
!pulsing && overlapping
? 'scale-105'
: '',
pulsing && overlapping
@@ -150,9 +159,29 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
</>
}
</div>
<div className="transition-[max-height] duration-200 ease-in-out">
{fields}
</div>
{
!showMore ? fields.slice(0, MAX_FIELDS) : fields
}
{fields.length > MAX_FIELDS && (
<div
className="flex h-8 cursor-pointer items-center gap-1 justify-center border-t border-default text-xs text-slate-500 transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
onClick={toggleShowMore}
>
{showMore ? (
<>
<ChevronUp className=" size-4" />
{t('table.show_less')}
</>
) : (
<>
<ChevronDown className="size-4" />
{t('table.show_more')}
</>
)}
</div>
)}
</Card>
+10
View File
@@ -1,4 +1,5 @@
import { FieldType } from "@/lib/schemas/field-schema";
import { v4 } from "uuid";
@@ -16,4 +17,13 @@ export const getNextSequence = (fields: FieldType[]): number => {
export const cloneField = ( field : FieldType) : FieldType => {
return {
...field ,
id : v4() ,
} as FieldType ;
}
+23 -1
View File
@@ -1,8 +1,12 @@
import { FieldType } from "@/lib/schemas/field-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { Node } from "@xyflow/react";
import ELK from "elkjs/lib/elk.bundled.js";
import { cloneField } from "./field";
import { v4 } from "uuid";
import { getTimestamp } from "./utils";
const elk = new ELK();
@@ -87,9 +91,27 @@ const getDefaultTableOverlapping = (table: TableType, tables: TableType[]): bool
return false;
}
const cloneTable = ( table: TableType) : TableType => {
return {
...table ,
id : v4() ,
name : `${table.name}_copy` ,
fields : table.fields.map((field : FieldType) => cloneField(field)) ,
posX : table.posX - 360 ,
createdAt : getTimestamp()
}
}
export {
adjustTablesPositions,
getDefaultTableOverlapping ,
isTablesOverlapping
isTablesOverlapping ,
cloneTable
}