rename database feature implemented

This commit is contained in:
KarimTamani
2025-06-03 22:22:56 +01:00
parent 90b24a7fc7
commit 3ab346efd9
14 changed files with 237 additions and 34 deletions
+3 -2
View File
@@ -45,18 +45,19 @@ const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHand
<DropdownItem
key={child.title as string}
className={child.theme == "danger" ? "text-danger" : ""}
color={child.theme}
shortcut={child.shortcut}
classNames={{
shortcut : "dark:border-font/10"
}}
className="text-font/90"
onPressEnd={ child.clickHandler }
>
{
!child.children ?
<div className="text-font/90">
<div >
{child.title}
{
child.divide &&
+5 -1
View File
@@ -59,7 +59,11 @@ const Menu: React.FC<MenuProps> = ({ }) => {
],
},
{ title: t("menu.export_orm_models"), divide: true },
{ title: t("menu.delete_project"), theme: "danger" },
{
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
open(Modals.DELETE_DATABASE)
}
},
],
},
{
+30 -12
View File
@@ -22,22 +22,23 @@ export interface ModalProps {
actionName?: string,
actionHandler?: () => void,
isDisabled?: boolean,
header?: string
header?: string,
variant?: "default" | "danger"
}
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled }) => {
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default" }) => {
const targetRef = React.useRef(null);
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
const [isLoading , setIsLoading] = useState<boolean>( false ) ;
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation();
const handleAction = async (onClose: () => void) => {
setIsLoading( true)
actionHandler && await actionHandler();
setIsLoading( false) ;
setIsLoading(true)
actionHandler && await actionHandler();
setIsLoading(false);
onClose()
}
return (
@@ -68,12 +69,29 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
</ModalBody>
<ModalFooter>
<div className="flex w-full justify-between">
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
{
variant == "default" &&
<>
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
</>
}
{
variant == "danger" &&
<>
<Button color="default" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
</>
}
</div>
</ModalFooter>
</>
+5 -1
View File
@@ -2,6 +2,7 @@
import Menu from "../menu/menu";
import ConnectionStatus from "./connection-status";
import RenameDatabase from "./rename-database";
interface Props {
@@ -23,9 +24,12 @@ const Navbar: React.FC<Props> = ({ }) => {
/>
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
<div className="ml-4 w-full">
<div className="flex w-full justify-between">
<div className="flex w-full justify-between items-center">
<Menu />
<div className=" w-full h-full flex items-center justify-center">
<RenameDatabase/>
</div>
<ConnectionStatus />
</div>
+109
View File
@@ -0,0 +1,109 @@
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import React, { useEffect, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { useTranslation } from "react-i18next";
import { Button, Divider, Image, Input } from "@heroui/react";
import { getDatabaseByDialect } from "@/lib/database";
import { Save } from "lucide-react";
const RenameDatabase: React.FC = ({ }) => {
const { database } = useDatabase();
const { editDatabase } = useDatabaseOperations();
const [editMode, setEditMode] = useState<boolean>(true);
const [dbName, setDbName] = useState<string>(database.name);
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
console.log (database.name) ;
setDbName(database.name);
}, [database.name])
const saveDatabaseName = async () => {
if (!dbName || dbName.trim().length == 0)
return;
setIsLoading(true);
await editDatabase({
id: database.id,
name: dbName
});
setIsLoading(false);
setEditMode(false);
}
return (
<div className="group">
{
!editMode &&
<Tooltip>
<TooltipTrigger asChild>
<label
className=" w-full text-editable truncate h-8 px-3 text-sm font-bold dark:text-white flex gap-4 items-center justify-center text-font/90 hover:underline"
onDoubleClick={() => setEditMode(true)}
>
<Image
src={getDatabaseByDialect(database.dialect).logo}
width={22}
className=" rounded-none "
/>
{dbName}
</label>
</TooltipTrigger>
<TooltipContent>
{t("navbar.rename_db")}
</TooltipContent>
</Tooltip>
}
{
editMode &&
<div className="flex justify-center items-center gap-4">
<Image
src={getDatabaseByDialect(database.dialect).logo}
width={32}
className=" rounded-none "
/>
<Input
placeholder={database.name}
type="text"
size="sm"
autoFocus
radius="sm"
variant="faded"
onChange={(event: any) => setDbName(event.target.value)}
value={dbName}
onBlur={saveDatabaseName}
className="h-8 w-full focus-visible:ring-0 shadow-none"
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
<Button
variant="solid"
className="size-6 p-0 text-white h-8 w-8"
size="sm"
color="primary"
onPressEnd={saveDatabaseName}
isIconOnly
isLoading={isLoading}
isDisabled={isLoading}
>
<Save className="size-3" />
</Button>
</div>
}
</div >
)
}
export default React.memo(RenameDatabase);
+8 -1
View File
@@ -12,6 +12,9 @@ export const en = {
color_picker: {
default_color: "Default color"
},
navbar : {
rename_db : "Rename database" ,
} ,
db_controller: {
filter: "Filter",
add_table: "Add Table",
@@ -116,6 +119,7 @@ export const en = {
show_docs: "Show Docs",
join_discord: "Join Discord",
},
modals: {
@@ -128,7 +132,10 @@ export const en = {
continue : "Continue" ,
open : "Open" ,
open_database : "Open Database" ,
open_database_header : "Open a database by selecting one from the list."
open_database_header : "Open a database by selecting one from the list." ,
delete_database : "Delete Database" ,
delete_database_content : "This action is irreversible and will permanently remove the diagram." ,
delete : "Delete"
}
}
+1 -1
View File
@@ -205,7 +205,7 @@ const DatabasePage: React.FC = () => {
/>
</Controls >
<Background className=" dark:bg-background-100" />
<Background className="bg-default/10 dark:bg-background-100" />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
@@ -0,0 +1,42 @@
import Modal, { ModalProps } from "@/components/modal/modal";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { currentDatabaseId } = useDatabase();
const { deleteDatabase , switchDatabase} = useDatabaseOperations();
const { t } = useTranslation();
const onDelete = useCallback(async () => {
return await new Promise(async (res, rej) => {
await deleteDatabase(currentDatabaseId as string);
switchDatabase( undefined ) ;
res(currentDatabaseId)
})
}, [currentDatabaseId])
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.delete_database")}
actionName={t("modals.delete")}
className="min-w-[560px]"
actionHandler={onDelete}
variant="danger"
>
<p className="text-font/90">
{t("modals.delete_database_content")}
</p>
</Modal>
)
}
export default DeleteDatabaseModal;
@@ -54,6 +54,7 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const normalizedPresent = normalizeDatabase(datatbaseState.present);
const differences = compare(normalizedDatabase, normalizedPresent);
if (differences && differences.length > 0) {
setIsProcessing(true);
@@ -30,7 +30,7 @@ interface DatabaseOperationsContextType {
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
deleteDatabase: (id: string) => Promise<void>,
switchDatabase: (databaseId: string) => void,
switchDatabase: (databaseId: string | undefined ) => void,
createTable: (table: TableInsertType) => Promise<void>,
editTable: (table: TableInsertType) => Promise<QueryResult>,
@@ -15,13 +15,13 @@ 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 }
const DatabaseProvider: React.FC<Props> = ({ children }) => {
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(undefined);
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(localStorage.getItem("database_id") as string | undefined);
// Fetch all databases
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
@@ -71,9 +71,12 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
);
const switchDatabase = useCallback((databaseId: string) => {
const switchDatabase = useCallback((databaseId: string | undefined) => {
setCurrentDatabaseId(databaseId);
localStorage.setItem("database_id", databaseId);
if (databaseId)
localStorage.setItem("database_id", databaseId);
else
localStorage.removeItem("database_id")
}, [])
// Normalize result to single object
@@ -111,12 +114,12 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const updateDbNumTables = useCallback(async (databaseId: string, tx: any) => {
const [{ count : numOfTables }] = await tx
const [{ count: numOfTables }] = await tx
.select({ count: count() })
.from(tables)
.where(eq(tables.databaseId, databaseId))
console.log (numOfTables)
console.log(numOfTables)
await tx.update(databaseModel).set({
numOfTables
}).where(eq(databaseModel.id, databaseId))
@@ -132,9 +135,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
databaseId: currentDatabaseId,
createdAt: table.createdAt || getTimestamp()
});
await updateDbNumTables(currentDatabaseId, tx);
if (table.fields) {
await tx.insert(fields).values(
@@ -279,7 +279,14 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
try {
await db.transaction(async (tx) => {
for (const operation of operations) {
if (operation.type == "CREATE_TABLE") {
if ( operation.type == "RENAME_DATABASE") {
currentDatabaseId && await tx.update(databaseModel).set({
name : operation.chnages.name
}).where(eq(databaseModel.id , currentDatabaseId)) ;
}
else 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)
@@ -393,7 +400,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
<DatabaseDataContext.Provider value={{
database: database as unknown as DatabaseType,
currentDatabaseId ,
currentDatabaseId,
databases: databases as DatabaseType[],
isLoading,
isSwitchingDatabase,
@@ -4,7 +4,8 @@ import { createContext } from "react";
export enum Modals {
CREATE_RELATIONSHIP = "CREATE_RELATIONSHIP",
CREATE_DATABASE = "CREATE_DATABASE" ,
OPEN_DATABASE = "OPEN_DATABASE"
OPEN_DATABASE = "OPEN_DATABASE" ,
DELETE_DATABASE = "DELETE_DATABASE"
}
interface ModalContextType {
@@ -6,6 +6,7 @@ import { useDisclosure } from "@heroui/react";
import CreateRelationshipModal from "@/pages/database/modals/create-relationship-modal";
import { CreateDatabaseModal } from "@/pages/database/modals/create-database-modal";
import OpenDatabaseModal from "@/pages/database/modals/open-database-modal";
import DeleteDatabaseModal from "@/pages/database/modals/delete-database-modal";
interface Props { children: React.ReactNode }
@@ -46,10 +47,13 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
<CreateRelationshipModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
currentModal.modal == Modals.CREATE_DATABASE &&
<CreateDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
<CreateDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
currentModal.modal == Modals.OPEN_DATABASE &&
<OpenDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
currentModal.modal == Modals.DELETE_DATABASE &&
<DeleteDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
) : undefined
}
{children}
+7 -2
View File
@@ -1,5 +1,5 @@
// Importing types from schema definitions
import { DatabaseType } from "@/lib/schemas/database-schema";
import { DatabaseInsertType, DatabaseType } from "@/lib/schemas/database-schema";
import { FieldType } from "@/lib/schemas/field-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
@@ -9,6 +9,7 @@ import { FieldIndexType } from "@/lib/schemas/field_index-schema";
// Define the possible operations that can be performed when diffing databases
export type DBDiffOperation =
| { type: "RENAME_DATABASE", chnages: DatabaseInsertType }
| { type: 'CREATE_TABLE'; table: TableType }
| { type: 'DELETE_TABLE'; tableId: string }
| { type: 'UPDATE_TABLE'; tableId: string; changes: Partial<TableType> }
@@ -53,8 +54,12 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
for (const op of patch) {
const parts = op.path.split('/').filter(Boolean); // Split JSON path into parts
if (op.op == "replace" && parts[0] == "name") {
operations.push({ type: "RENAME_DATABASE", chnages: { name: op.value } as DatabaseInsertType })
}
// Handle table-related changes
if (parts[0] == "tables") {
else if (parts[0] == "tables") {
const tableId = parts[1];
// Whole table operations (add/delete table)