mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
rename database feature implemented
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user