Release / publish (, ubuntu-22.04) (push) Failing after 14m39s
Release / publish (--target aarch64-apple-darwin, macos-latest) (push) Canceled after 0s
Release / publish (--target x86_64-apple-darwin, macos-15-intel) (push) Canceled after 0s
Release / publish (, windows-latest) (push) Canceled after 0s
* feat(models): Change::RebuildTable + role/privilege/readiness/maintenance structs (Task 1.1) * feat(introspection): role/privilege/tablespace/rebuild-readiness query builders (Task 1.2) * feat(lib): rebuild_table payload, table/role object kinds, v0.7.7 capabilities + types (Task 1.3) * feat(object_crud): table_ddl builder (create/edit-diff/options/drop) + build_ddl table arm (Task 2.1) * feat(object_crud): role_ddl builder (create/edit/drop/grant/revoke) + build_ddl role arm (Task 2.2) * feat(object_crud): rebuild_script assembler + fk/grant/sequence introspection queries (Task 2.3) * feat(commands): transactional RebuildTable arm + build_rebuild_script + get_table_columns commands (Task 2.4) * feat(commands): get_roles/privileges/readiness/tablespaces + run_maintenance (Task 2.5) * feat(commands): role/privilege/rebuild/maintenance typed wrappers (Task 3.1) * feat(store): roles slice + openFormTab table/role dedup (Task 3.2) * feat(ui): TableForm (create/edit-diff/rebuild) + cross-schema FK with ON DELETE/UPDATE (Task 4.1) * feat(ui): RoleForm + RoleDetail + RoleGrantsEditor (Task 4.2) * feat(ui): wire roles into object registry + explorer (Task 4.3) * feat(ui): Edit Table + maintenance menu + Create Table toolbar + rebuild badge (Task 4.4) * chore: bump version to 0.7.7 (Task 5.1) * docs: sync README/ROADMAP/AGENTS to v0.7.7 (Task 5.2) * test: v0.7.7 manual smoke checklist (Task 5.3) * chore: sync Cargo.lock to v0.7.7 * style(ui): match TableForm visual layout to shared object-form rows (remove stray padding) * feat(ui): table creator grid redesign + per-column FK slide-in panel with relationship controls * style(ui): table-creator columns grid — bordered cells like the data table view * feat(ui): table creator — type dropdown, PK/Nullable rules, drag-to-reorder columns (vertical), tidy add-column row * feat(ui): table creator — single-PK exclusivity, name on top, fixed column widths + contained h-scroll, no-wrap constraints * style(ui): table creator — constraints column sized to content (no clip), overscroll containment * style(ui): table creator — overscrollBehavior none on both scroll containers, add-column row py-2 * feat(ui): table creator — centered add-column button, schema dropdown auto-selected to current schema * style(ui): table creator — columns grid fills container width, scrolls only when too narrow * feat(ui): table creator — shorthand PG types in dropdown, Auto-Increment only for int-family * feat(ui): FK panel — animate in/out from the right, flush create-table-style rows * fix(ui): FK panel — smooth slide (willChange + eased), local column options from the form grid * feat(ui): FK composer (multi-column, type preview, gated sections) + composite PK + constraints cog in actions col + auto-named FKs * fix(ui): FK panel blocks unnamed table with hint; Foreign keys section lists staged + DB FKs * style(ui): FK panel — transparent full-width selects, title↔select borders, 2-col column picker * style(ui): FK panel — vertical divider between local/ref columns * style(ui): FK panel — section title padding py-2 * style(ui): FK panel — remove row margins + select/text padding in column picker * style(ui): FK panel — flush column picker (no section padding), border-r + py-1 cells * style(ui): FK panel — cell padding + row borders on column picker (user polish) * feat: FK inline into CREATE TABLE (single staged change in create mode); touch up class * style(ui): hide FK icon on PK columns; fix grid header/row column alignment * feat(ui): SQL preview in table creator uses read-only Monaco editor * feat(ui): Foreign keys section — relation rows with icon + Edit/Remove actions * style(ui): FK relation row — label, icon, table on one line * style(ui): FormRow selects transparent like FK panel; test matches FK relation label * style(ui): form-row focus outline amber-400/20 (subtle) * style(ui): SQL view — remove px-4 py-3 wrapper padding * feat(ui): Create Table button label; auto tree refresh after schema-modifying commits; table name on staged ddl card * fix(roles): cast rolconnlimit::int8 to fix panic deserializing int4 into i64 * fix(privileges): cast privilege_type::text to avoid domain-array panic; aclexplode for sequences/schemas (role_sequence_grants removed in PG15, schema_privileges never existed) * feat(ui): role privileges sections collapsible (collapsed by default) with 5-entry preview + fade + Show more * feat(ui): role privileges — collapsed state shows first 5 with fade; expand reveals all * chore(test): drop unused fireEvent import in RoleDetail test * style(ui): hide chevron when section has 5 or fewer entries * docs: README/ROADMAP/AGENTS — v0.7.7 shipped details (roles/grants, table editor, FK composer, privilege explorer); ROADMAP adds v0.7.8 Next up (quick wins) * docs(readme): collapse Recent changes and Key features into details blocks * docs(readme): move hero caption under image; align download + comparison table columns * docs(readme): collapse only body content — keep Recent Changes / Key Features headings visible * docs(readme): per-subsection collapsibles with meaningful summary labels
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { DependencyDialog } from "../DependencyDialog";
|
|
import {
|
|
buildObjectDdl,
|
|
dropCrudParams,
|
|
initialCrudParams,
|
|
type CrudItem,
|
|
type ObjectKind,
|
|
} from "../../../lib/objectCrud";
|
|
import { getObjectDependencies } from "../../../lib/commands";
|
|
import type { DependencyInfo } from "../../../lib/types";
|
|
import { useDbViewerStore } from "../../../stores/dbViewerStore";
|
|
|
|
interface Props {
|
|
connectionId: string;
|
|
objectType: ObjectKind;
|
|
item: CrudItem;
|
|
onRefresh: () => void;
|
|
/** Controlled open state (e.g. driven by a row's right-click). */
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
/** Extra non-CRUD actions appended below the divider (Copy DDL, Dependencies…). */
|
|
extraItems?: { id: string; label: string; danger?: boolean; onClick: () => void }[];
|
|
}
|
|
|
|
export const DROP_TITLE: Record<ObjectKind, string> = {
|
|
sequence: "sequence",
|
|
enum: "type",
|
|
view: "view",
|
|
extension: "extension",
|
|
index: "index",
|
|
constraint: "constraint",
|
|
function: "function",
|
|
procedure: "procedure",
|
|
trigger: "trigger",
|
|
table: "table",
|
|
role: "role",
|
|
};
|
|
|
|
export function ObjectContextMenu({
|
|
connectionId,
|
|
objectType,
|
|
item,
|
|
onRefresh,
|
|
open,
|
|
onOpenChange,
|
|
extraItems,
|
|
}: Props) {
|
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
const [deps, setDeps] = useState<DependencyInfo[] | null>(null);
|
|
const addChange = useDbViewerStore((s) => s.addChange);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isOpen = open ?? internalOpen;
|
|
const setOpen = (v: boolean) => {
|
|
if (onOpenChange) onOpenChange(v);
|
|
else setInternalOpen(v);
|
|
};
|
|
|
|
// Track the current open state in a ref so the document-level outside-click
|
|
// handler (registered once) only closes a menu that is actually open. Without
|
|
// this, closed instances would fire onOpenChange(false) on every mousedown
|
|
// and clobber the shared open key in the controlled (row right-click) case.
|
|
const openRef = useRef(isOpen);
|
|
openRef.current = isOpen;
|
|
|
|
useEffect(() => {
|
|
const h = (e: MouseEvent) => {
|
|
if (
|
|
openRef.current &&
|
|
menuRef.current &&
|
|
!menuRef.current.contains(e.target as Node)
|
|
) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", h);
|
|
return () => document.removeEventListener("mousedown", h);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const kind = objectType;
|
|
|
|
const openFormTab = (mode: "create" | "edit") => {
|
|
const title = `${mode === "create" ? "Create" : "Edit"} ${kind}`;
|
|
const description = `${mode === "create" ? "Create" : "Edit"} ${
|
|
item.name || kind
|
|
}`;
|
|
useDbViewerStore.getState().openFormTab({
|
|
kind,
|
|
schema:
|
|
mode === "create"
|
|
? useDbViewerStore.getState().currentSchema ?? item.schema ?? "public"
|
|
: item.schema,
|
|
name: mode === "edit" ? item.name : "",
|
|
title,
|
|
description,
|
|
mode,
|
|
params: initialCrudParams(kind, item, mode),
|
|
});
|
|
setOpen(false);
|
|
};
|
|
|
|
const startDrop = async () => {
|
|
setOpen(false);
|
|
let d: DependencyInfo[] = [];
|
|
try {
|
|
d = await getObjectDependencies(connectionId, item.schema, kind, item.name);
|
|
} catch {
|
|
d = [];
|
|
}
|
|
setDeps(d);
|
|
};
|
|
const confirmDrop = async () => {
|
|
const sqls = await buildObjectDdl(
|
|
connectionId,
|
|
kind,
|
|
dropCrudParams(kind, item),
|
|
);
|
|
sqls.forEach((sql) =>
|
|
addChange({
|
|
type: "ddl",
|
|
sql,
|
|
description: `Drop ${DROP_TITLE[kind]} ${item.name}`,
|
|
}),
|
|
);
|
|
setDeps(null);
|
|
onRefresh();
|
|
};
|
|
|
|
return (
|
|
<div ref={menuRef} className="relative">
|
|
<button
|
|
onClick={() => setOpen(!isOpen)}
|
|
aria-label="actions"
|
|
className="text-text-muted hover:text-text cursor-pointer"
|
|
>
|
|
⋮
|
|
</button>
|
|
{isOpen && (
|
|
<div className="absolute right-0 top-6 z-20 w-40 rounded-lg border border-border bg-surface py-1 text-sm text-text shadow-lg">
|
|
<button
|
|
onClick={() => openFormTab("create")}
|
|
className="block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer"
|
|
>
|
|
Create…
|
|
</button>
|
|
<button
|
|
onClick={() => openFormTab("edit")}
|
|
className="block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer"
|
|
>
|
|
Edit…
|
|
</button>
|
|
<button
|
|
onClick={startDrop}
|
|
className="block w-full text-left px-3 py-1.5 text-red-400 hover:bg-border/30 cursor-pointer"
|
|
>
|
|
Drop…
|
|
</button>
|
|
{extraItems && extraItems.length > 0 && (
|
|
<>
|
|
<div className="my-1 border-t border-border" />
|
|
{extraItems.map((it) => (
|
|
<button
|
|
key={it.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
it.onClick();
|
|
}}
|
|
className={`block w-full text-left px-3 py-1.5 hover:bg-border/30 cursor-pointer ${
|
|
it.danger ? "text-red-400" : ""
|
|
}`}
|
|
>
|
|
{it.label}
|
|
</button>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
<DependencyDialog
|
|
open={!!deps}
|
|
deps={deps ?? []}
|
|
onProceed={confirmDrop}
|
|
onCancel={() => setDeps(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
} |