From 84d086c585d5d9001361d1d1a770964c80998ac9 Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Tue, 13 May 2025 23:11:48 +0100 Subject: [PATCH] Relationship and Tables in diagram integrated with the API --- .../cardinality-marker/cardinality-marker.tsx | 60 ++++ src/components/color-picker/color-picker.tsx | 36 ++- .../hooks/use-relationship-name.tsx | 4 +- src/hooks/use-relationship-to-edge.tsx | 26 ++ src/hooks/use-table-to-node.tsx | 28 ++ src/i18/languages/en.ts | 5 + src/lib/colors.ts | 21 +- src/lib/schemas/app-schema.ts | 3 +- src/lib/schemas/data-type-schema.ts | 8 +- src/lib/schemas/field-schema.ts | 10 +- src/pages/database/database-page.tsx | 282 +++++++----------- .../relationship-accordion-body.tsx | 12 +- .../relationship-accordion-header.tsx | 58 +++- .../table-accordion-item/field/field-item.tsx | 6 +- .../table-accordion-body.tsx | 9 +- .../table-accordion-header.tsx | 24 +- .../tables-controller/tables-controller.tsx | 1 - src/pages/database/table/field.tsx | 98 ++++-- src/pages/database/table/relationship.tsx | 98 +++--- src/pages/database/table/table.tsx | 113 ++++--- .../database-provider/database-context.ts | 16 +- .../database-provider/database-provider.tsx | 44 ++- src/styles/globals.css | 9 +- src/utils/field.ts | 8 +- tailwind.config.js | 40 +-- ....timestamp-1747149074318-60c8d6378ece5.mjs | 25 ++ 26 files changed, 696 insertions(+), 348 deletions(-) create mode 100644 src/components/cardinality-marker/cardinality-marker.tsx rename src/{components => }/hooks/use-relationship-name.tsx (54%) create mode 100644 src/hooks/use-relationship-to-edge.tsx create mode 100644 src/hooks/use-table-to-node.tsx create mode 100644 vite.config.ts.timestamp-1747149074318-60c8d6378ece5.mjs diff --git a/src/components/cardinality-marker/cardinality-marker.tsx b/src/components/cardinality-marker/cardinality-marker.tsx new file mode 100644 index 0000000..57bff86 --- /dev/null +++ b/src/components/cardinality-marker/cardinality-marker.tsx @@ -0,0 +1,60 @@ +export interface CardinalityMarkerProps { + + selected?: boolean, + direction?: "start" | "end", + type: "one" | "many" +} + + + +const CardinalityMarker: React.FC = ({ selected = false, type, direction = "start" }) => { + + const id = `${type}_${direction}${selected ? "_selected" : ""}`; + const renderMarker = () => { + if (type == "many") { + if (direction == "start") + return () + else if (direction == "end") + return () + + } + + if (type == "one") { + if (direction == "start") { + return () + } + if (direction == "end") { + return () + } + } + } + return ( + <> + + + { + renderMarker() + } + + + + ) + +} + + +export default CardinalityMarker; \ No newline at end of file diff --git a/src/components/color-picker/color-picker.tsx b/src/components/color-picker/color-picker.tsx index dc2d981..80760c1 100644 --- a/src/components/color-picker/color-picker.tsx +++ b/src/components/color-picker/color-picker.tsx @@ -1,22 +1,26 @@ import { colorOptions } from "@/lib/colors"; import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react"; +import { Slash } from "lucide-react"; import { useCallback, useState } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip"; +import { useTranslation } from "react-i18next"; interface ColorPickerProps { defaultColor?: string; - onChange?: (color: string) => void + onChange?: (color: string | undefined) => void } const ColorPicker: React.FC = ({ defaultColor, onChange }) => { - const [color , setColor] = useState( defaultColor as string ) ; + const [color, setColor] = useState(defaultColor as string); const [isOpen, setIsOpen] = useState(false); + const { t } = useTranslation(); - const onSelect = useCallback((color: string) => { + const onSelect = useCallback((color: string | undefined) => { setIsOpen(false); - onChange && onChange(color) ; - setColor ( color) + onChange && onChange(color); + setColor(color) }, [onChange]) return ( @@ -26,7 +30,7 @@ const ColorPicker: React.FC = ({ defaultColor, onChange }) => className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md" isIconOnly style={{ - backgroundColor : color + backgroundColor: color ? color : undefined }} > @@ -46,6 +50,26 @@ const ColorPicker: React.FC = ({ defaultColor, onChange }) => ))} + + +
onSelect(undefined)} + > + + +
+
+ + {t("color_picker.default_color")} + +
+ + diff --git a/src/components/hooks/use-relationship-name.tsx b/src/hooks/use-relationship-name.tsx similarity index 54% rename from src/components/hooks/use-relationship-name.tsx rename to src/hooks/use-relationship-name.tsx index 8609c81..5b28a13 100644 --- a/src/components/hooks/use-relationship-name.tsx +++ b/src/hooks/use-relationship-name.tsx @@ -6,7 +6,9 @@ import { useTranslation } from "react-i18next"; export const useRelationshipName = (relationship: RelationshipType) => { const { t } = useTranslation(); const name: string = useMemo(() => { - return `${relationship.sourceTable.name}_${relationship.sourceField.name} - ${relationship.targetTable.name}_${relationship.targetField.name}_fk` + if ( !relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField) + return "" ; + return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk` }, [relationship, t]) return { name diff --git a/src/hooks/use-relationship-to-edge.tsx b/src/hooks/use-relationship-to-edge.tsx new file mode 100644 index 0000000..e6ba4d2 --- /dev/null +++ b/src/hooks/use-relationship-to-edge.tsx @@ -0,0 +1,26 @@ + +import { RelationshipType } from "@/lib/schemas/relationship-schema"; +import { LEFT_PREFIX, TARGET_PREFIX } from "@/pages/database/table/field"; + +import { Edge, useReactFlow } from "@xyflow/react"; +import { useEffect } from "react"; + +export const useRelationshipToEdge = (relationships: RelationshipType[]): void => { + const { setEdges } = useReactFlow(); + useEffect(() => { + const edges = relationships.map((relationship: RelationshipType) => { + return { + id: relationship.id, + source: relationship.sourceTableId, + sourceHandle: LEFT_PREFIX + relationship.sourceFieldId, + target: relationship.targetTableId, + targetHandle: TARGET_PREFIX + relationship.targetFieldId, + data : { + relationship + } + } as Edge + }) + setEdges(edges) + }, [relationships]) + +} \ No newline at end of file diff --git a/src/hooks/use-table-to-node.tsx b/src/hooks/use-table-to-node.tsx new file mode 100644 index 0000000..baa8db5 --- /dev/null +++ b/src/hooks/use-table-to-node.tsx @@ -0,0 +1,28 @@ + +import { TableType } from "@/lib/schemas/table-schema"; +import { Node, useReactFlow } from "@xyflow/react"; +import { useEffect } from "react"; + +export const useTableToNode = (tables: TableType[]): void => { + const { setNodes } = useReactFlow(); + useEffect(() => { + const nodes = tables.map((table: TableType) => { + return { + id: table.id, + type: "table", + position: { + x: table.posX, + y: table.posY + }, + data: { + table + }, + style: { + width: 224 + } + } as Node + }) + setNodes(nodes) + }, [tables]) + +} \ No newline at end of file diff --git a/src/i18/languages/en.ts b/src/i18/languages/en.ts index cc2bdc2..2c51298 100644 --- a/src/i18/languages/en.ts +++ b/src/i18/languages/en.ts @@ -12,6 +12,9 @@ export const en = { close: "Close", create: "Create" }, + color_picker : { + default_color : "Default color" + } , db_controller: { filter: "Filter", add_table: "Add Table", @@ -53,6 +56,8 @@ export const en = { delete: "Delete", field_setting: "Field Setting", table_actions: "Field Actions", + actions : "Actions" , + field_note: "Field note", delete_field: "Delete Field", diff --git a/src/lib/colors.ts b/src/lib/colors.ts index fa0cda5..5db6713 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -1,9 +1,8 @@ - export const colorOptions = [ "#fd7f6f", // Warm coral red "#7eb0d5", // Soft sky blue "#b2e061", // Fresh lime green - "#bd7ebe", // Soft lavender pink + "#ffb55a", // Vibrant orange "#ffee65", // Warm yellow "#beb9db", // Light purple grey @@ -16,6 +15,24 @@ export const colorOptions = [ ]; +/* + +export const colorOptions = [ + '#ff6363', // A brighter red. + '#ff6b8a', // A vibrant pink. + '#c05dcf', // A rich purple. + '#b067e9', // A lighter purple. + '#8a61f5', // A bold indigo. + '#7175fa', // A lighter indigo. + '#8eb7ff', // A sky blue. + '#42e0c0', // A fresh aqua. + '#4dee8a', // A mint green. + '#9ef07a', // A lime green. + '#ffe374', // A warm yellow. + '#ff9f74', // A peachy orange. +]; +*/ + export const randomColor = () => { return colorOptions[Math.floor(Math.random() * colorOptions.length)]; }; \ No newline at end of file diff --git a/src/lib/schemas/app-schema.ts b/src/lib/schemas/app-schema.ts index e7fadd0..65d405b 100644 --- a/src/lib/schemas/app-schema.ts +++ b/src/lib/schemas/app-schema.ts @@ -1,6 +1,6 @@ import { DrizzleAppSchema, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver'; -import { data_types } from './data-type-schema'; +import { data_types, dataTypeRelations } from './data-type-schema'; import { tables, tablesRelations } from './table-schema'; import { fields, fieldsRelations } from './field-schema'; import { relationshipRelations, relationships } from './relationship-schema'; @@ -15,6 +15,7 @@ export const drizzleSchema = { tablesRelations , fieldsRelations , relationshipRelations , + dataTypeRelations }; // Infer the PowerSync schema from your Drizzle schema diff --git a/src/lib/schemas/data-type-schema.ts b/src/lib/schemas/data-type-schema.ts index 3c1218f..f704cec 100644 --- a/src/lib/schemas/data-type-schema.ts +++ b/src/lib/schemas/data-type-schema.ts @@ -1,5 +1,6 @@ -import { InferSelectModel } from 'drizzle-orm'; +import { InferSelectModel, relations } from 'drizzle-orm'; import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { fields } from './field-schema'; export const data_types = sqliteTable('data_types', { id : text("id") , @@ -8,4 +9,9 @@ export const data_types = sqliteTable('data_types', { +export const dataTypeRelations = relations(fields, ({ many }) => ({ + fields : many(fields) , +})); + + export interface DataType extends InferSelectModel { }; \ No newline at end of file diff --git a/src/lib/schemas/field-schema.ts b/src/lib/schemas/field-schema.ts index 88bd226..d41d226 100644 --- a/src/lib/schemas/field-schema.ts +++ b/src/lib/schemas/field-schema.ts @@ -1,6 +1,6 @@ import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core'; import { tables } from './table-schema'; -import { data_types } from './data-type-schema'; +import { data_types, DataType } from './data-type-schema'; import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm'; import { relationships } from './relationship-schema'; @@ -25,8 +25,11 @@ export const fieldsRelations = relations(fields, ({ one }) => ({ table: one(tables, { fields: [fields.tableId], references: [tables.id], - }), + type : one(data_types , { + fields : [ fields.typeId] , + references : [data_types.id] + }) , sourceRelations: one(relationships), targetRelations: one(relationships), @@ -34,7 +37,8 @@ export const fieldsRelations = relations(fields, ({ one }) => ({ export interface FieldType extends InferSelectModel { - sequence: number + sequence: number , + type : DataType }; diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index d0a6613..1a21cce 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -1,209 +1,153 @@ - -import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react"; -import { useCallback, useEffect, useMemo } from "react"; + +import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Connection, Controls, Edge, EdgeChange, EdgeRemoveChange, MiniMap, Node, NodeChange, NodePositionChange, NodeRemoveChange, NodeSelectionChange, OnEdgesChange, OnNodesChange, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react"; +import { SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import Table from "./table/table"; import '@xyflow/react/dist/style.css'; -import { Relationship } from "./table/relationship"; -import DBController from "./db-controller/db-controller"; -import { Table as TableType} from "@/lib/schemas/table-schema"; - +import Relationship from "./table/relationship"; +import DBController from "./db-controller/db-controller"; +import { TableInsertType, TableType } from "@/lib/schemas/table-schema"; +import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useTableToNode } from "@/hooks/use-table-to-node"; +import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge"; +import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; +import { v4 } from "uuid"; +import { LEFT_PREFIX, TARGET_PREFIX } from "./table/field"; +import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker"; + interface DatabaseProps { initialTables?: TableType } - -const initialNodes = [ - { - id: '1', type: "table", position: { x: 500, y: 500 }, data: { - - table: { - id: "1", - name: "users", - fields: [ - { - id: 'e5n46eojbyxpg65pb1sesysi2', - name: 'id', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: true, - unique: true, - nullable: false, - createdAt: Date.now(), - }, - { - id: '7nbmg2bt6dzltr8rfouxctdlc', - name: 'reservation_id', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: true, - createdAt: Date.now(), - }, - { - id: 'f9kcpflp010xe5ad3rmexla63', - name: 'rating', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: false, - createdAt: Date.now(), - }, - { - id: '3znekob0pqusyvdoq14nhlvgo', - name: 'comment', - type: { - id: 'character_varying', - name: 'character varying', - }, - primaryKey: false, - unique: false, - nullable: true, - createdAt: Date.now(), - }, - ], - } - }, - style: { width: 224 } - }, - - { - id: '2', type: "table", position: { x: 200, y: 500 }, data: { - - table: { - id: "1", - name: "products", - fields: [ - { - id: 'e5n46eojbyxpg65pb1sesysi2', - name: 'id', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: true, - unique: true, - nullable: false, - createdAt: Date.now(), - }, - { - id: '7nbmg2bt6dzltr8rfouxctdlc', - name: 'user_id', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: true, - createdAt: Date.now(), - }, - { - id: 'f9kcpflp010xe5ad3rmexla63', - name: 'rating', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: false, - createdAt: Date.now(), - }, - { - id: 'f9kcpflp010xe5ad3rmexla63', - name: 'rating', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: false, - createdAt: Date.now(), - }, - { - id: 'f9kcpflp010xe5ad3rmexla63', - name: 'rating', - type: { - id: 'integer', - name: 'integer', - }, - primaryKey: false, - unique: false, - nullable: false, - createdAt: Date.now(), - }, - { - id: '3znekob0pqusyvdoq14nhlvgo', - name: 'comment', - type: { - id: 'character_varying', - name: 'character varying', - }, - primaryKey: false, - unique: false, - nullable: true, - createdAt: Date.now(), - }, - ], - } - }, - style: { width: 224 } - }, -]; - -const initialEdges: any[] = []; - - const edgeTypes = { 'relationship-edge': Relationship, }; const DatabasePage: React.FC = ({ initialTables }) => { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const { tables, relationships, updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabase(); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + + useTableToNode(tables); + useRelationshipToEdge(relationships); - const nodeTypes = useMemo(() => ({ table: Table }), []); - const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + const onConnect = useCallback((connection: Connection) => { + + createRelationship({ + id: v4(), + sourceTableId: connection.source, + targetTableId: connection.target, + sourceFieldId: (connection.sourceHandle as string).split("_").pop(), + targetFieldId: (connection.targetHandle as string).replace(TARGET_PREFIX, "") + } as RelationshipInsertType) + + setEdges((eds) => addEdge(connection, eds)); + + }, [setEdges]); + + const handleNodesChanges: OnNodesChange = useCallback((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"); + + if (nodePositionChanges.length > 0) + updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({ + id: change.id, + posX: change.position?.x, + posY: change.position?.y + } as TableInsertType))); + if (nodeRemoveChanges.length > 0) + deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id)); + + onNodesChange(changes); + + }, [onNodesChange]); + + + const handleEdgeChanges: OnEdgesChange = useCallback((changes: EdgeChange[]) => { + const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[]; + if (edgeRemoveChanges.length > 0) { + deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id)) + } + onEdgesChange(changes as EdgeChange[]); + }, [onEdgesChange]); + + + const selectedTableIds: string[] = useMemo(() => { + return nodes.filter((node: Node) => node.selected).map((node: Node) => node.id) + }, [nodes]); + + const selectedRelationshipIds: string[] = useMemo(() => { + return relationships.filter((relationship: RelationshipType) => + selectedTableIds.includes(relationship.sourceTableId) || + selectedTableIds.includes(relationship.targetTableId) + ).map((relationship: RelationshipType) => relationship.id); + }, [selectedTableIds, relationships]); + + + useEffect(() => { + setEdges((edges: any) => { + return edges.map((edge: any) => { + const selected: boolean = selectedRelationshipIds.includes(edge.id); + return { + ...edge , + animated : selected , + + } as Edge + }) + + }) + }, [setEdges, selectedRelationshipIds]) return (
+ - + + + + + + + + + + + + + + + + + +
) } diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx index 167fb2a..140242b 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx @@ -37,6 +37,9 @@ const RelationshipAccordionBody: React.FC = ({ r const removeRelationship = () => { deleteRelationship(relationship.id); } + + if (!relationship.sourceTable || !relationship.targetTable) + return; return (
@@ -48,12 +51,11 @@ const RelationshipAccordionBody: React.FC = ({ r - {relationship.sourceTable.name}({relationship.sourceField.name}) + {relationship.sourceTable?.name}({relationship.sourceField?.name}) - - {relationship.sourceTable.name}({relationship.sourceField.name}) + {relationship.sourceTable?.name}({relationship.sourceField?.name})
@@ -68,11 +70,11 @@ const RelationshipAccordionBody: React.FC = ({ r - {relationship.targetTable.name}({relationship.targetField.name}) + {relationship.targetTable?.name}({relationship.targetField?.name}) - {relationship.targetTable.name}({relationship.targetField.name}) + {relationship.targetTable?.name}({relationship.targetField?.name})
diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx index 12429fb..31c2dde 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx @@ -1,9 +1,8 @@ -import { useRelationshipName } from "@/components/hooks/use-relationship-name"; -import { randomColor } from "@/lib/colors"; +import { useRelationshipName } from "@/hooks/use-relationship-name"; import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; import { useDatabase } from "@/providers/database-provider/database-provider"; -import { Button, cn, Input } from "@heroui/react"; -import { Check, ChevronRight, EllipsisVertical, Focus, Pencil } from "lucide-react"; +import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react"; +import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -19,8 +18,9 @@ const RelationshipAccordionHeader: React.FC = const [editMode, setEditMode] = useState(false); const { name: defaultName } = useRelationshipName(relationship); - const { editRelationship } = useDatabase(); - + const { editRelationship, deleteRelationship } = useDatabase(); + const { t } = useTranslation(); + const [popOverOpen, setPopOverOpen] = useState(false); const [name, setName] = useState(relationship.name ? relationship.name : defaultName) @@ -33,6 +33,11 @@ const RelationshipAccordionHeader: React.FC = setEditMode(false); } + const onDeleteRelationship = () => { + + deleteRelationship(relationship.id); + setPopOverOpen(false); + } return (
=
- + + + + + + + + +
+

+ {t("db_controller.actions")} +

+
+
+ + + } + > + {t("db_controller.delete")} + + +
+
}
diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx index 3e31c1b..2826a1f 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx @@ -6,7 +6,7 @@ import { EllipsisVertical, GripVertical, KeyRound, Trash2 } from "lucide-react" import { useTranslation } from "react-i18next"; import { CSS } from "@dnd-kit/utilities"; import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"; -import { Key , useState } from "react"; +import { Key , useEffect, useState } from "react"; import { useDatabase } from "@/providers/database-provider/database-provider"; import Autocomplete from "@/components/auto-complete/auto-complete"; import ToggleButton from "@/components/toggle/toggle"; @@ -32,6 +32,10 @@ const FieldItem: React.FC = ({ field }) => { transform: CSS.Transform.toString(transform), }; + useEffect(() => { + setFieldName(field.name) ; + } , [field.name]) + const removeField = () => { setPopOverOpen(false); deleteField(field.id) diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx index bbd037e..ee012e4 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx @@ -24,8 +24,9 @@ const TableAccordionBody: React.FC = ({ table }) => { const { t } = useTranslation(); const { editTable, createField } = useDatabase(); - const onColorChange = useCallback((color: string) => { - editTable({ id: table.id, color } as TableType); + const onColorChange = useCallback((color: string | undefined) => { + console.log(color) + editTable({ id: table.id, color: color ? color : null } as TableType); }, [table]); const addField = (event: any) => { @@ -42,8 +43,8 @@ const TableAccordionBody: React.FC = ({ table }) => { const saveNote = () => { editTable({ - id : table.id , - note + id: table.id, + note } as TableType) } diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx index cccfad4..b80406a 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx @@ -1,8 +1,8 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip"; -import { Button, cn, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react"; +import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react"; import { Check, ChevronRight, Copy, EllipsisVertical, FileKey, FileType, Focus, Pencil, Trash } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { TableType } from "@/lib/schemas/table-schema"; @@ -19,15 +19,16 @@ export interface TableAccordionHeaderProps { const TableAccordionHeader: React.FC = ({ table, isOpen }) => { const { editTable, deleteTable, createField } = useDatabase(); const [popOverOpen, setPopOverOpen] = useState(false); - const style = { - borderLeft: "6px solid " + table.color, - }; - const [tableName, setTableName] = useState(table.name); const { t } = useTranslation(); const [editMode, setEditMode] = useState(false); + + useEffect(()=> { + setTableName(table.name) ; + } , [table.name]) + const saveTableName = async () => { await editTable({ id: table.id, name: tableName }); setEditMode(false); @@ -50,11 +51,12 @@ const TableAccordionHeader: React.FC = ({ table, isOp nullable: true, }) } - - + return ( -
= ({ table, isOp variant="bordered" onBlur={saveTableName} type="text" - className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm " + className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 dark:bg-slate-900 text-sm " /> + } { editMode && <> setEditMode(false)} + + onBlur={saveFieldName} placeholder={field.name} autoFocus type="text" - // value={fieldName} + value={fieldName} onClick={(e) => e.stopPropagation()} - // onChange={(e) => setFieldName(e.target.value)} - className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm " + onChange={(e) => setFieldName(e.target.value)} + + className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm " /> } +
= ({ field, showHandles }) => {
{ @@ -89,7 +139,7 @@ const Field: React.FC = ({ field, showHandles }) => { !connection.inProgress ? "invisible" : "visible" )} > -export const Relationship: React.FC> = ({ - id, - sourceX, - sourceY, - targetX, - targetY, - source, - target, - selected, - data, -}) => { +const Relationship: React.FC> = (props) => { + + let { id, sourceX, sourceY, targetX, targetY, source, target, selected, data , animated } = props; const { getInternalNode, getEdge } = useReactFlow(); - //const { openRelationshipFromSidebar, selectSidebarSection } = useLayout(); - //const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff(); + const sourceNode = getInternalNode(source); const targetNode = getInternalNode(target); const edge = getEdge(id); const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.( - "right-" + "right_" ) ? 'right' : 'left'; const sourceWidth = sourceNode?.measured.width ?? 0; - const sourceLeftX = - sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10; - const sourceRightX = - sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX; + const sourceLeftX = sourceHandle === 'left' ? sourceX + 6 : sourceX - sourceWidth - 10; + const sourceRightX = sourceHandle === 'left' ? sourceX + sourceWidth + 10 : sourceX; const targetWidth = targetNode?.measured.width ?? 0; - const targetLeftX = targetX - 1; - const targetRightX = targetX + targetWidth + 10; + const targetLeftX = targetX - 2; + const targetRightX = targetX + targetWidth + 3; + + const { sourceSide, targetSide } = useMemo(() => { const distances = { @@ -84,12 +77,12 @@ export const Relationship: React.FC> = ({ sourceY, targetX: targetSide === 'left' ? targetLeftX : targetRightX, targetY, - borderRadius: 14, + borderRadius: 6, sourcePosition: sourceSide === 'left' ? Position.Left : Position.Right, targetPosition: targetSide === 'left' ? Position.Left : Position.Right, - // offset: (edgeNumber + 1) * 14, + }), [ sourceSide, @@ -100,20 +93,37 @@ export const Relationship: React.FC> = ({ targetRightX, sourceY, targetY, - // edgeNumber, + ] ); + const { startMarker, endMarker } = useMemo(() => { + const cardinality: Cardinality = data?.relationship.cardinality as Cardinality; + if (cardinality) { + const cardinalities: string[] = cardinality.split("_to_"); + return { + startMarker: `${cardinalities[0]}_${"start"}${selected ? "_selected" : ""}`, + endMarker: `${cardinalities[1]}_${"end"}${selected ? "_selected" : ""}` + } + } + return { + startMarker: `${"one"}_${"start"}${selected ? "_selected" : ""}`, + endMarker: `${"many"}_${"end"}${selected ? "_selected" : ""}` + } + }, [data?.relationship.cardinality, selected]); + + return ( <> + { @@ -122,13 +132,18 @@ export const Relationship: React.FC> = ({ // openRelationshipInEditor(); } }} + style={{ + strokeDasharray: (selected || animated) ? '5, 5' : '0', + animation: (selected || animated) ? 'dash 0.5s linear infinite' : 'none', + }} /> + { if (e.detail === 2) { @@ -136,5 +151,20 @@ export const Relationship: React.FC> = ({ } }} /> + ) -} \ No newline at end of file +} + +export default Relationship; + +/* + + + + + + + + + +*/ \ No newline at end of file diff --git a/src/pages/database/table/table.tsx b/src/pages/database/table/table.tsx index eb06f38..780cac6 100644 --- a/src/pages/database/table/table.tsx +++ b/src/pages/database/table/table.tsx @@ -1,8 +1,8 @@ -import { Table as TableType } from "@/lib/interfaces/table"; -import { Handle, Node, NodeProps, NodeResizer, Position } from "@xyflow/react"; -import React, { useCallback, useState } from 'react'; +import { Edge, Handle, Node, NodeProps, NodeResizer, Position, useReactFlow, useStore } from "@xyflow/react"; + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Card, CardBody, CardHeader, cn, Divider, Input } from "@heroui/react"; import { ChevronsLeftRight, @@ -16,10 +16,16 @@ import { SquarePlus, SquareMinus, Divide, + Focus, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"; -import { Field } from "@/lib/interfaces/field"; + import FieldComponent from "./field"; +import { FieldType } from "@/lib/schemas/field-schema"; +import { TableType } from "@/lib/schemas/table-schema"; +import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useTranslation } from "react-i18next"; +import { RelationshipType } from "@/lib/schemas/relationship-schema"; export const MAX_TABLE_SIZE = 450; @@ -38,40 +44,64 @@ const Table: React.FC> = React.memo(({ id, data: { table, }, }) => { - const [editMode, setEditMode] = useState(false); - + const [editMode, setEditMode] = useState(false); + const [tableName, setTableName] = useState(table.name); + const { editTable } = useDatabase(); + const { t } = useTranslation(); + + + useEffect(() => { + setTableName(table.name); + }, [table.name]) + + const saveTableName = async () => { + await editTable({ id: table.id, name: tableName }); + setEditMode(false); + } + const edges = useStore((store) => Array.from(store.edges.values())) as Edge[]; + + const highlightedEdges: Edge[] = useMemo(() => { + return edges.filter((edge: Edge) => edge.animated || edge.selected); + }, [edges]); + + + + console.log(highlightedEdges) return ( -
-
- - -
- + )} + shadow="sm" + > +
+
+
+
+ { editMode && <> setEditMode(false)} + onChange={(event: any) => setTableName(event.target.value)} + value={tableName} + onBlur={saveTableName} type="text" - className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm " + className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm " />
-
-
- {table.fields.map((field: Field) => ( - - ))} -
+
+ {table.fields.map((field: FieldType) => { + + const highlight: boolean = highlightedEdges.find((edge: any) => + (edge.data?.relationship as RelationshipType).sourceFieldId == field.id || + (edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null; + + + return () + })}
-
+ + ) }); -Table.displayName = 'table'; + export default Table; diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index 0e13843..ed904dd 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -11,21 +11,25 @@ import { createContext } from "react"; export interface DatabaseContextType { tables: TableType[], - data_types: DataType[] , - relationships : RelationshipType[] , + data_types: DataType[], + relationships: RelationshipType[], // table operations createTable: (table: TableInsertType) => Promise, editTable: (table: TableInsertType) => Promise, deleteTable: (id: string) => Promise, + updateTablePositions: (tables: TableInsertType[]) => Promise , + deleteMultiTables: (ids: string[]) => Promise // field operations createField: (field: FieldInsertType) => Promise, editField: (field: FieldInsertType) => Promise, deleteField: (id: string) => Promise, - orderTableFields: (fields: FieldType[]) => Promise , + orderTableFields: (fields: FieldType[]) => Promise, // relationship operations - createRelationship : ( relationship : RelationshipInsertType) => Promise , - editRelationship : ( relationship : RelationshipInsertType) => Promise , - deleteRelationship : ( id : string) => Promise , + createRelationship: (relationship: RelationshipInsertType) => Promise, + editRelationship: (relationship: RelationshipInsertType) => Promise, + deleteRelationship: (id: string) => Promise, + deleteMultiRelationships: (ids: string[]) => Promise + } diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 7ff765a..7111aa6 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -5,7 +5,7 @@ import { db, powerSyncDb } from "../sync-provider/sync-provider"; import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema"; import { useQuery } from "@powersync/react"; import { toCompilableQuery } from "@powersync/drizzle-driver"; -import { asc, desc, eq, sql } from "drizzle-orm"; +import { asc, desc, eq, inArray, sql } from "drizzle-orm"; import { QueryResult } from "@powersync/web"; import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema"; import { RelationshipInsertType, relationships, RelationshipType } from "@/lib/schemas/relationship-schema"; @@ -16,11 +16,14 @@ interface Props { children: React.ReactNode } const DatabaseProvider: React.FC = ({ children }) => { - const { data: tablesList, isLoading, error } = useQuery(toCompilableQuery( + const { data: tablesList } = useQuery(toCompilableQuery( db.query.tables.findMany({ with: { fields: { orderBy: asc(fields.sequence), + with: { + type: true + } } }, orderBy: desc(tables.createdAt) @@ -53,6 +56,9 @@ const DatabaseProvider: React.FC = ({ children }) => { const deleteTable = async (id: string): Promise => { return await db.delete(tables).where(eq(tables.id, id)); } + const deleteMultiTables = async (ids: string[]): Promise => { + return await db.delete(tables).where(inArray(tables.id, ids )) + } const createField = async (field: FieldInsertType): Promise => { return await db.insert(fields).values(field); @@ -90,13 +96,44 @@ const DatabaseProvider: React.FC = ({ children }) => { return await db.delete(relationships).where(eq(relationships.id, id)); } + const deleteMultiRelationships = async (ids: string[]): Promise => { + return await db.delete(relationships).where(inArray(relationships.id, ids )) + } + const updateTablePositions = async (tables: TableInsertType[]): Promise => { + + const posXCases = tables + .map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posX}`) + .join('\n '); + + const posYCases = tables + .map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posY}`) + .join('\n '); + + const ids = tables.map((table: TableInsertType) => `'${table.id}'`).join(',\n '); + + const sql = ` + UPDATE tables + SET + posX = CASE id + ${posXCases} + END, + posY = CASE id + ${posYCases} + END + WHERE id IN ( + ${ids} + );`; + return await powerSyncDb.execute(sql); + } return ( = ({ children }) => { createRelationship, editRelationship, deleteRelationship, + deleteMultiRelationships , tables: tablesList as TableType[], - relationships : relationshipsList as RelationshipType[] , + relationships: relationshipsList as RelationshipType[], data_types }}> {children} diff --git a/src/styles/globals.css b/src/styles/globals.css index b7d6c34..899af16 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,7 +5,7 @@ .text-editable { - @apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer; + @apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary-700 rounded-md cursor-pointer; } @@ -51,6 +51,9 @@ div[data-slot="content"] hr[role="separator"] { } -button[data-slot="trogger"] { - background-color: red !important; + +@keyframes dash { + to { + stroke-dashoffset: -10; + } } \ No newline at end of file diff --git a/src/utils/field.ts b/src/utils/field.ts index 6832fe5..5bcf60f 100644 --- a/src/utils/field.ts +++ b/src/utils/field.ts @@ -1,5 +1,5 @@ import { FieldType } from "@/lib/schemas/field-schema"; - + @@ -12,4 +12,8 @@ export const getNextSequence = (fields: FieldType[]): number => { return field.sequence > max.sequence ? field : max }); return maxSequenceItem.sequence + 1; -} \ No newline at end of file +} + + + + \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 9d43fea..d3290a0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,17 +19,17 @@ export default { foreground: 'hsl(240 5.3% 26.1%)', }, primary: { - DEFAULT: "#961FFE", - 50: '#961FFE0D', // 5% opacity - 100: '#961FFE1A', // 10% opacity - 200: '#961FFE33', // 20% opacity - 300: '#961FFE4D', // 30% opacity - 400: '#961FFE66', // 40% opacity - 500: '#961FFE80', // 50% opacity - 600: '#961FFE99', // 60% opacity - 700: '#961FFEB3', // 70% opacity + DEFAULT: "#9822ff", + 50: '#9822ff0D', // 5% opacity + 100: '#9822ff1A', // 10% opacity + 200: '#9822ff33', // 20% opacity + 300: '#9822ff4D', // 30% opacity + 400: '#9822ff66', // 40% opacity + 500: '#9822ff80', // 50% opacity + 600: '#9822ff99', // 60% opacity + 700: '#9822ffB3', // 70% opacity 800: '#961FFFCC', // 80% opacity - 900: '#961FFEE6', // 90% opacity + 900: '#9822ffE6', // 90% opacity }, }, }, @@ -50,17 +50,17 @@ export default { } , primary: { - DEFAULT: "#961FFE", - 50: '#961FFE0D', // 5% opacity - 100: '#961FFE1A', // 10% opacity - 200: '#961FFE33', // 20% opacity - 300: '#961FFE4D', // 30% opacity - 400: '#961FFE66', // 40% opacity - 500: '#961FFE80', // 50% opacity - 600: '#961FFE99', // 60% opacity - 700: '#961FFEB3', // 70% opacity + DEFAULT: "#9822ff", + 50: '#9822ff0D', // 5% opacity + 100: '#9822ff1A', // 10% opacity + 200: '#9822ff33', // 20% opacity + 300: '#9822ff4D', // 30% opacity + 400: '#9822ff66', // 40% opacity + 500: '#9822ff80', // 50% opacity + 600: '#9822ff99', // 60% opacity + 700: '#9822ffB3', // 70% opacity 800: '#961FFFCC', // 80% opacity - 900: '#961FFEE6', // 90% opacity + 900: '#9822ffE6', // 90% opacity }, }, }, diff --git a/vite.config.ts.timestamp-1747149074318-60c8d6378ece5.mjs b/vite.config.ts.timestamp-1747149074318-60c8d6378ece5.mjs new file mode 100644 index 0000000..b215b24 --- /dev/null +++ b/vite.config.ts.timestamp-1747149074318-60c8d6378ece5.mjs @@ -0,0 +1,25 @@ +// vite.config.ts +import { defineConfig } from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite/dist/node/index.js"; +import react from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import topLevelAwait from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-plugin-top-level-await/exports/import.mjs"; +var vite_config_default = defineConfig({ + plugins: [react(), tsconfigPaths(), topLevelAwait()], + server: { + port: 3e3 + }, + optimizeDeps: { + // Don't optimize these packages as they contain web workers and WASM files. + // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 + exclude: ["@journeyapps/wa-sqlite", "@powersync/web"], + include: ["@powersync/web > js-logger"] + }, + worker: { + format: "es", + plugins: () => [topLevelAwait()] + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90YW1hbi9PbmVEcml2ZS9EZXNrdG9wL3N0YWNrcmVuZGVyL3N0YWNrcmVuZGVyL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCdcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnXG5pbXBvcnQgdG9wTGV2ZWxBd2FpdCBmcm9tICd2aXRlLXBsdWdpbi10b3AtbGV2ZWwtYXdhaXQnO1xuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpLCB0c2NvbmZpZ1BhdGhzKCkgLCB0b3BMZXZlbEF3YWl0KCldLFxuICBzZXJ2ZXIgOiB7IFxuICAgIHBvcnQgOiAzMDAwXG4gIH0gLCBcbiAgb3B0aW1pemVEZXBzOiB7XG4gICAgLy8gRG9uJ3Qgb3B0aW1pemUgdGhlc2UgcGFja2FnZXMgYXMgdGhleSBjb250YWluIHdlYiB3b3JrZXJzIGFuZCBXQVNNIGZpbGVzLlxuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS92aXRlanMvdml0ZS9pc3N1ZXMvMTE2NzIjaXNzdWVjb21tZW50LTE0MTU4MjA2NzNcbiAgICBleGNsdWRlOiBbJ0Bqb3VybmV5YXBwcy93YS1zcWxpdGUnLCAnQHBvd2Vyc3luYy93ZWInXSxcbiAgICBpbmNsdWRlOiBbJ0Bwb3dlcnN5bmMvd2ViID4ganMtbG9nZ2VyJ11cbiAgfSxcbiAgd29ya2VyOiB7XG4gICAgZm9ybWF0OiAnZXMnLFxuICAgIHBsdWdpbnM6ICgpID0+IFsgdG9wTGV2ZWxBd2FpdCgpXVxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxVyxTQUFTLG9CQUFvQjtBQUNsWSxPQUFPLFdBQVc7QUFDbEIsT0FBTyxtQkFBbUI7QUFDMUIsT0FBTyxtQkFBbUI7QUFFMUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sR0FBRyxjQUFjLEdBQUksY0FBYyxDQUFDO0FBQUEsRUFDcEQsUUFBUztBQUFBLElBQ1AsTUFBTztBQUFBLEVBQ1Q7QUFBQSxFQUNBLGNBQWM7QUFBQTtBQUFBO0FBQUEsSUFHWixTQUFTLENBQUMsMEJBQTBCLGdCQUFnQjtBQUFBLElBQ3BELFNBQVMsQ0FBQyw0QkFBNEI7QUFBQSxFQUN4QztBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sUUFBUTtBQUFBLElBQ1IsU0FBUyxNQUFNLENBQUUsY0FBYyxDQUFDO0FBQUEsRUFDbEM7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=