mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
search for table & relationships functionalities implemented
This commit is contained in:
@@ -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`
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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={
|
||||
|
||||
+1
-9
@@ -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}
|
||||
|
||||
|
||||
+38
-11
@@ -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} />
|
||||
|
||||
+19
-7
@@ -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
|
||||
|
||||
@@ -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" : ""
|
||||
)}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user