From 90b24a7fc71a1cea213ca160ffa17e43821574c3 Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Mon, 2 Jun 2025 20:09:42 +0100 Subject: [PATCH] open database feature is done --- src/hooks/use-table-to-node.tsx | 6 +- src/i18/index.ts | 10 +- src/pages/database/database-page.tsx | 11 +-- .../tables-controller/tables-controller.tsx | 2 + .../database/modals/create-database-modal.tsx | 4 +- .../database/modals/open-database-modal.tsx | 23 +++-- .../database-provider/database-context.ts | 10 +- .../database-provider/database-provider.tsx | 91 ++++++++++++++----- src/styles/globals.css | 55 +++++++++-- 9 files changed, 149 insertions(+), 63 deletions(-) diff --git a/src/hooks/use-table-to-node.tsx b/src/hooks/use-table-to-node.tsx index 32c1287..bb93ce2 100644 --- a/src/hooks/use-table-to-node.tsx +++ b/src/hooks/use-table-to-node.tsx @@ -4,12 +4,12 @@ import { Node, useReactFlow } from "@xyflow/react"; import { useEffect } from "react"; import { getDefaultTableOverlapping } from "@/utils/tables"; import hash from "object-hash"; +import { useDatabase } from "@/providers/database-provider/database-provider"; export const useTableToNode = (tables: TableType[]): void => { const { setNodes } = useReactFlow(); - - + useEffect(() => { const tableNodes = tables.map((table: TableType) => { @@ -33,7 +33,7 @@ export const useTableToNode = (tables: TableType[]): void => { }) setNodes((nodes) => { - + return tableNodes.map((tableNode) => { const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id); if (!node) diff --git a/src/i18/index.ts b/src/i18/index.ts index 0e78df9..7f24947 100644 --- a/src/i18/index.ts +++ b/src/i18/index.ts @@ -1,34 +1,26 @@ import { en, enLanguage } from "./languages/en"; import { fr, frLanguage } from "./languages/fr"; import { Language } from "./types"; - import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { ar, arLanguage } from "./languages/ar"; - - export const languages: Language[] = [ enLanguage, frLanguage , arLanguage ]; - - const resources = { en, fr, ar }; - - - i18n.use(LanguageDetector) .use(initReactI18next) .init({ resources , - lng: 'fr', + lng: 'en', interpolation: { escapeValue: false, diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index 297d498..5f562a1 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -7,7 +7,7 @@ import { } from "@xyflow/react"; // React built-ins -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; // Custom components and styles import Table from "./table/table"; @@ -55,7 +55,7 @@ const DatabasePage: React.FC = () => { const { setIsConnectionInProgress } = useDiagramOps(); // Destructure tables and relationships from database - const { tables, relationships } = database; + const { tables, relationships } = database; // Hook to allow zooming and centering the diagram const { fitView } = useReactFlow(); @@ -63,7 +63,7 @@ const DatabasePage: React.FC = () => { // Define custom node and edge types const nodeTypes = useMemo(() => ({ table: Table }), []); const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []); - + // Called when a connection is made between fields const onConnect = useCallback((connection: Connection) => { const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop(); @@ -113,12 +113,11 @@ const DatabasePage: React.FC = () => { } as TableInsertType))); // Delete tables if removed - if (nodeRemoveChanges.length > 0) { - + if (nodeRemoveChanges.length > 0 ) { deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id)); } return onNodesChange(changes); - }, [onNodesChange]); + }, [onNodesChange ]); // Called when edges (relationships) change const handleEdgeChanges: OnEdgesChange = useCallback((changes: EdgeChange[]) => { diff --git a/src/pages/database/db-controller/tables-controller/tables-controller.tsx b/src/pages/database/db-controller/tables-controller/tables-controller.tsx index e3901d0..f03dbf8 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -10,6 +10,7 @@ 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 { randomColor } from "@/lib/colors"; interface Props { } @@ -45,6 +46,7 @@ const TablesController: React.FC = ({ }) => { name: `table_${tables.length + 1}`, posX, posY, + fields: [{ id: v4(), name: "id", diff --git a/src/pages/database/modals/create-database-modal.tsx b/src/pages/database/modals/create-database-modal.tsx index c3739ce..9d7c89a 100644 --- a/src/pages/database/modals/create-database-modal.tsx +++ b/src/pages/database/modals/create-database-modal.tsx @@ -14,7 +14,7 @@ export const CreateDatabaseModal: React.FC = ({ isOpen, onOpenChange const { t } = useTranslation(); const [selectedDbType, setSelectedDbType] = useState([DBTypes[0].dialect]); const [dbName, setDbName] = useState("db_example"); - const { createDatabase, setCurrentDatabaseId } = useDatabaseOperations(); + const { createDatabase, switchDatabase } = useDatabaseOperations(); const onDatabaseTypeChange = (types: string[]) => { const selectedType: string | undefined = types.pop(); @@ -31,7 +31,7 @@ export const CreateDatabaseModal: React.FC = ({ isOpen, onOpenChange name: dbName, dialect: selectedDbType[0] as any }); - setCurrentDatabaseId(databaseId); + switchDatabase(databaseId); res(databaseId) }) diff --git a/src/pages/database/modals/open-database-modal.tsx b/src/pages/database/modals/open-database-modal.tsx index 4aa4216..40b81f9 100644 --- a/src/pages/database/modals/open-database-modal.tsx +++ b/src/pages/database/modals/open-database-modal.tsx @@ -15,15 +15,16 @@ import { useTranslation } from "react-i18next"; const OpenDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => { const { t } = useTranslation(); - const { databases } = useDatabase(); - const { setCurrentDatabaseId } = useDatabaseOperations(); - const [selectedDatabase, setSelectedDatabase] = useState(undefined); + const { databases , currentDatabaseId} = useDatabase(); + const { switchDatabase } = useDatabaseOperations(); + const [selectedDatabase, setSelectedDatabase] = useState( + (() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined) + ); + const openDatabase = () => { - setCurrentDatabaseId( selectedDatabase.currentKey) + switchDatabase( selectedDatabase.currentKey) } - - return ( = ({ isOpen, onOpenChange }) => { selectedKeys={selectedDatabase} onSelectionChange={setSelectedDatabase} classNames={{ - wrapper: "min-h-[360px] shadow-none border-1 border-divider border-sm" + wrapper: "min-h-[360px] shadow-none border-1 border-divider rounded-sm" , + th : "dark:bg-background" , + tr : "hover:bg-default-100 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 " }} > - - Dialect + + Dialect Name Created at Tables @@ -63,7 +66,7 @@ const OpenDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => { width={24} /> - {database.name} + {database.name} { new Date(database.createdAt as string).toLocaleString("en-US") } diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index ed1fa3c..8b7ac4b 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -15,8 +15,10 @@ import { createContext } from "react"; interface DatabaseDataContextType { database: DatabaseType, - databases : DatabaseType[] , + currentDatabaseId: string | undefined, + databases: DatabaseType[], isLoading: boolean, + isSwitchingDatabase: boolean, getField: (tableId: string, id: string) => FieldType | undefined, } @@ -27,14 +29,14 @@ interface DatabaseOperationsContextType { // database operations createDatabase: (database: DatabaseInsertType) => Promise, editDatabase: (database: DatabaseInsertType) => Promise, - deleteDatabase : ( id : string) => Promise , - setCurrentDatabaseId : ( id : string) => void , + deleteDatabase: (id: string) => Promise, + switchDatabase: (databaseId: string) => void, createTable: (table: TableInsertType) => Promise, editTable: (table: TableInsertType) => Promise, deleteTable: (id: string) => Promise, updateTablePositions: (tables: TableInsertType[]) => Promise, - deleteMultiTables: (ids: string[]) => Promise + deleteMultiTables: (ids: string[]) => Promise // field operations createField: (field: FieldInsertType) => Promise, editField: (field: FieldInsertType) => Promise, diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 7f9055e..4242d58 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -4,8 +4,8 @@ import { db } 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, inArray, or } from "drizzle-orm"; -import { QueryResult } from "@powersync/web"; +import { asc, count, desc, eq, inArray, or } from "drizzle-orm"; +import { QueryResult, Transaction } from "@powersync/web"; import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema"; import { RelationshipInsertType, relationships } from "@/lib/schemas/relationship-schema"; import DatabaseHistoryProvider from "../database-history/database-history-provider"; @@ -15,6 +15,7 @@ import { getTimestamp } from "@/utils/utils"; import { IndexInsertType, indices } from "@/lib/schemas/index-schema"; import { field_indices } from "@/lib/schemas/field_index-schema"; import { v4 } from "uuid"; +import { SQLiteTransaction } from "drizzle-orm/sqlite-core"; interface Props { children: React.ReactNode } @@ -23,17 +24,17 @@ const DatabaseProvider: React.FC = ({ children }) => { const [currentDatabaseId, setCurrentDatabaseId] = useState(undefined); // Fetch all databases - const { data: databases, isLoading: loadingDatabases , isFetching : fetchingDatabases} = useQuery(toCompilableQuery( + const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery( db.query.databases.findMany() )); // Fetch all data types - const { data: data_types, isLoading: loadingDataTypes , isFetching : fetchingDataTypes} = useQuery(toCompilableQuery( + const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery( db.query.data_types.findMany() )); // Fetch the current database with nested tables, fields, and relationships - let { data: database, isLoading: loadingCurrentDatabase , isFetching : fetchingCurrentDatabase} = useQuery( + let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery( toCompilableQuery( db.query.databases.findMany({ where: (databases, { eq }) => eq(databases.id, currentDatabaseId as string), @@ -68,9 +69,13 @@ const DatabaseProvider: React.FC = ({ children }) => { }) ) ); - - - + + + const switchDatabase = useCallback((databaseId: string) => { + setCurrentDatabaseId(databaseId); + localStorage.setItem("database_id", databaseId); + }, []) + // Normalize result to single object if (database.length == 1) database = database[0] as any; @@ -78,11 +83,16 @@ const DatabaseProvider: React.FC = ({ children }) => { // Auto-select first database if none is selected useEffect(() => { if (databases.length > 0 && !currentDatabaseId) { - setCurrentDatabaseId(databases[0].id); + switchDatabase(databases[0].id); } - }, [currentDatabaseId, databases]) + }, [currentDatabaseId, databases]); + + const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase; + const isSwitchingDatabase: boolean = useMemo(() => { + return isFetching && currentDatabaseId != (database as any)?.id + }, [currentDatabaseId, database, isFetching]); + - const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase ; // CRUD for database const createDatabase = useCallback(async (database: DatabaseInsertType): Promise => { return await db.insert(databaseModel).values({ @@ -99,6 +109,20 @@ const DatabaseProvider: React.FC = ({ children }) => { await db.delete(databaseModel).where(eq(databaseModel.id, id)); }, [db]); + const updateDbNumTables = useCallback(async (databaseId: string, tx: any) => { + + const [{ count : numOfTables }] = await tx + .select({ count: count() }) + .from(tables) + .where(eq(tables.databaseId, databaseId)) + + console.log (numOfTables) + await tx.update(databaseModel).set({ + numOfTables + }).where(eq(databaseModel.id, databaseId)) + }, []); + + // CRUD operations for Tables const createTable = useCallback(async (table: TableInsertType): Promise => { if (currentDatabaseId) { @@ -108,6 +132,10 @@ const DatabaseProvider: React.FC = ({ children }) => { databaseId: currentDatabaseId, createdAt: table.createdAt || getTimestamp() }); + + + + await updateDbNumTables(currentDatabaseId, tx); if (table.fields) { await tx.insert(fields).values( table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id })) @@ -124,12 +152,28 @@ const DatabaseProvider: React.FC = ({ children }) => { }, [db]); const deleteTable = useCallback(async (id: string): Promise => { - await db.delete(tables).where(eq(tables.id, id)); - }, [db]); + if (currentDatabaseId) { + await db.transaction(async (tx) => { + await tx.delete(tables).where(eq(tables.id, id)); + await updateDbNumTables(currentDatabaseId, tx) + }) + } else { + throw Error("No Database selected"); + } - const deleteMultiTables = useCallback(async (ids: string[]): Promise => { - return await db.delete(tables).where(inArray(tables.id, ids)); - }, [db]); + }, [db, currentDatabaseId]); + + const deleteMultiTables = useCallback(async (ids: string[]): Promise => { + if (currentDatabaseId) { + await db.transaction(async (tx) => { + await tx.delete(tables).where(inArray(tables.id, ids));; + await updateDbNumTables(currentDatabaseId, tx) + }) + } else { + throw Error("No Database selected"); + } + + }, [db, currentDatabaseId]); // CRUD operations for Fields const createField = useCallback(async (field: FieldInsertType): Promise => { @@ -191,7 +235,6 @@ const DatabaseProvider: React.FC = ({ children }) => { id: v4(), fieldId, indexId, - }))) }) }, [db]) @@ -234,11 +277,11 @@ const DatabaseProvider: React.FC = ({ children }) => { // Apply a list of diff operations to sync database const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => { try { - console.log ("excuted diff operations") await db.transaction(async (tx) => { for (const operation of operations) { if (operation.type == "CREATE_TABLE") { await tx.insert(tables).values(operation.table); + currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx); if (operation.table.fields && Object.values(operation.table.fields).length > 0) await tx.insert(fields).values(Object.values(operation.table.fields)); @@ -246,7 +289,9 @@ const DatabaseProvider: React.FC = ({ children }) => { await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId)); } else if (operation.type === "DELETE_TABLE") { + await tx.delete(tables).where(eq(tables.id, operation.tableId)); + currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx); } else if (operation.type === "CREATE_FIELD") { await tx.insert(fields).values(operation.field); @@ -292,14 +337,14 @@ const DatabaseProvider: React.FC = ({ children }) => { } catch (error) { throw error } - }, [db]); + }, [db, currentDatabaseId]); const databaseOpsValue = useMemo(() => ({ createDatabase, editDatabase, deleteDatabase, - setCurrentDatabaseId, + switchDatabase, createTable, editTable, deleteTable, @@ -348,13 +393,17 @@ const DatabaseProvider: React.FC = ({ children }) => { { - !isLoading && database && + database && !isLoading && !isSwitchingDatabase && {children} diff --git a/src/styles/globals.css b/src/styles/globals.css index b4493d9..c4eb24a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,5 +1,3 @@ - - .react-flow__attribution { display: none; } @@ -121,17 +119,58 @@ div[data-slot="content"] hr[role="separator"] { } svg.text-icon:hover { - - color : #333639 + + color: #333639 } -.dark .bg-background-50 { - background-color: red !important; +.dark .bg-background-50 { + background-color: red !important; } -.database-checkbox[data-selected="true"]{ - border : 1.5px solid hsl(var(--heroui-primary-300)); +.database-checkbox[data-selected="true"] { + border: 1.5px solid hsl(var(--heroui-primary-300)); +} + + + +thead[role="rowgroup"] th { + border-radius: 0px !important; + +} + +thead[role="rowgroup"] th:last-child { + border-radius: 0px 4px 4px 0px !important; + +} + +thead[role="rowgroup"] th:first-child { + border-radius: 4px 0px 0px 4px !important; + +} + + + + +tbody[role="rowgroup"] tr td::before { + + display: none; +} + + +tbody[role="rowgroup"] tr td { + border-radius: 0px; +} + + +tbody[role="rowgroup"] tr td:last-child { + border-radius: 0px 4px 4px 0px !important; + +} + +tbody[role="rowgroup"] tr td:first-child { + border-radius: 4px 0px 0px 4px !important; + } \ No newline at end of file