refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Plus, MoreVertical } from "lucide-react";
|
||||
import * as cmd from "../../lib/commands";
|
||||
import type { DependencyInfo } from "../../lib/types";
|
||||
import { AnimatedModal } from "../ui/AnimatedModal";
|
||||
import { Button } from "../ui/Button";
|
||||
import { DependencyDialog } from "./DependencyDialog";
|
||||
|
||||
interface SchemaMenuProps {
|
||||
connectionId: string;
|
||||
schema?: string;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export function SchemaMenu({ connectionId, schema, onRefresh }: SchemaMenuProps) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [newName, setNewName] = useState("");
|
||||
|
||||
const [dropOpen, setDropOpen] = useState(false);
|
||||
const [deps, setDeps] = useState<DependencyInfo[]>([]);
|
||||
const [typed, setTyped] = useState("");
|
||||
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [menuOpen]);
|
||||
|
||||
const resetErrors = () => setErr(null);
|
||||
|
||||
const create = async () => {
|
||||
resetErrors();
|
||||
try {
|
||||
await cmd.createSchema(connectionId, name);
|
||||
setCreating(false);
|
||||
setName("");
|
||||
onRefresh();
|
||||
} catch (e) {
|
||||
setErr((e as Error)?.message ?? String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const rename = async () => {
|
||||
resetErrors();
|
||||
if (!schema) return;
|
||||
try {
|
||||
await cmd.renameSchema(connectionId, schema, newName);
|
||||
setRenaming(false);
|
||||
setNewName("");
|
||||
onRefresh();
|
||||
} catch (e) {
|
||||
setErr((e as Error)?.message ?? String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const startDrop = async () => {
|
||||
setMenuOpen(false);
|
||||
if (!schema) return;
|
||||
try {
|
||||
const d = await cmd.getObjectDependencies(connectionId, schema, "schema", schema);
|
||||
setDeps(d);
|
||||
setDropOpen(true);
|
||||
setTyped("");
|
||||
setErr(null);
|
||||
} catch (e) {
|
||||
setErr((e as Error)?.message ?? String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDrop = async (cascade: boolean) => {
|
||||
if (!schema) return;
|
||||
try {
|
||||
await cmd.dropSchema(connectionId, schema, cascade);
|
||||
setDropOpen(false);
|
||||
setTyped("");
|
||||
setDeps([]);
|
||||
onRefresh();
|
||||
} catch (e) {
|
||||
setErr((e as Error)?.message ?? String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const hasDeps = deps.length > 0;
|
||||
|
||||
const inputClass =
|
||||
"w-full bg-surface border border-border rounded-md px-3 py-2 text-sm text-text placeholder:text-text-muted outline-none focus:border-accent/50 transition-colors";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1" ref={menuRef}>
|
||||
<button
|
||||
aria-label="New schema"
|
||||
onClick={() => {
|
||||
setCreating(true);
|
||||
setName("");
|
||||
setErr(null);
|
||||
}}
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-text-muted hover:text-text hover:bg-surface-raised cursor-pointer"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
|
||||
{schema && (
|
||||
<button
|
||||
aria-label="Schema menu"
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-text-muted hover:text-text hover:bg-surface-raised cursor-pointer"
|
||||
>
|
||||
<MoreVertical size={14} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{menuOpen && (
|
||||
<div className="absolute right-0 top-8 mt-1 rounded-xl bg-surface border border-border py-1 z-20 min-w-[160px] shadow-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
setRenaming(true);
|
||||
setNewName("");
|
||||
setErr(null);
|
||||
}}
|
||||
className="flex items-center px-3 py-2 text-sm w-full text-left transition-colors cursor-pointer text-text-muted hover:text-text hover:bg-surface-raised"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDrop}
|
||||
className="flex items-center px-3 py-2 text-sm w-full text-left transition-colors cursor-pointer text-red-400 hover:bg-red-500/10 hover:text-red-300"
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New Schema */}
|
||||
<AnimatedModal open={creating} onClose={() => setCreating(false)}>
|
||||
<div className="w-80">
|
||||
<h3 className="font-heading text-text text-lg mb-3">New Schema</h3>
|
||||
<p className="text-sm text-text-muted mb-3">Create a new schema in the current database.</p>
|
||||
<input
|
||||
placeholder="Schema name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
{err && <p className="text-red-400 text-xs mt-2">{err}</p>}
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<Button variant="ghost" onClick={() => setCreating(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={create}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
|
||||
{/* Rename Schema */}
|
||||
<AnimatedModal open={renaming} onClose={() => setRenaming(false)}>
|
||||
<div className="w-80">
|
||||
<h3 className="font-heading text-text text-lg mb-3">Rename {schema}</h3>
|
||||
<p className="text-sm text-text-muted mb-3">Enter the new name for this schema.</p>
|
||||
<input
|
||||
placeholder="New name"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
{err && <p className="text-red-400 text-xs mt-2">{err}</p>}
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<Button variant="ghost" onClick={() => setRenaming(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={rename}>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
|
||||
{/* Dependency warning */}
|
||||
{dropOpen && (
|
||||
<DependencyDialog
|
||||
open={dropOpen}
|
||||
deps={deps}
|
||||
onCancel={() => {
|
||||
setDropOpen(false);
|
||||
setTyped("");
|
||||
setDeps([]);
|
||||
setErr(null);
|
||||
}}
|
||||
onProceed={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Typed-name confirmation for CASCADE drop */}
|
||||
{dropOpen && hasDeps && (
|
||||
<AnimatedModal
|
||||
open={dropOpen}
|
||||
onClose={() => {
|
||||
setDropOpen(false);
|
||||
setTyped("");
|
||||
setErr(null);
|
||||
}}
|
||||
>
|
||||
<div className="w-80">
|
||||
<h3 className="font-heading text-text text-lg mb-3">Drop Schema: {schema}</h3>
|
||||
<p className="text-sm text-text-muted mb-3">
|
||||
Type the schema name to confirm the CASCADE drop.
|
||||
</p>
|
||||
<input
|
||||
placeholder="Type the schema name"
|
||||
value={typed}
|
||||
onChange={(e) => {
|
||||
setTyped(e.target.value);
|
||||
if (err) setErr(null);
|
||||
}}
|
||||
className={inputClass}
|
||||
/>
|
||||
{err && <p className="text-red-400 text-xs mt-2">{err}</p>}
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setDropOpen(false);
|
||||
setTyped("");
|
||||
setErr(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (typed !== schema) {
|
||||
setErr("Name does not match");
|
||||
return;
|
||||
}
|
||||
void confirmDrop(true);
|
||||
}}
|
||||
>
|
||||
Drop Schema
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
)}
|
||||
|
||||
{/* Empty-schema confirmation (no deps) */}
|
||||
{dropOpen && !hasDeps && (
|
||||
<AnimatedModal
|
||||
open={dropOpen}
|
||||
onClose={() => {
|
||||
setDropOpen(false);
|
||||
setErr(null);
|
||||
}}
|
||||
>
|
||||
<div className="w-80">
|
||||
<h3 className="font-heading text-text text-lg mb-3">Drop Schema: {schema}</h3>
|
||||
<p className="text-sm text-text-muted mb-3">No dependencies — drop this empty schema?</p>
|
||||
{err && <p className="text-red-400 text-xs mt-2">{err}</p>}
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setDropOpen(false);
|
||||
setErr(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => void confirmDrop(false)}>
|
||||
Drop Schema
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user