* chore: bump version to 0.5.0 (Task 1) * feat(store): v7 migration — favorites + recent_connections (Task 2) * feat(models): add favorite to Connection + Store CRUD (Task 3) * feat(types): ColumnInfo editability + IndexInfo/ConstraintInfo/RecentConnection (Task 4) * chore: bump version to 0.5.0 (Task 1) — lockfile * feat(commands): typed wrappers for favorites/recents/indexes/constraints (Task 5) * feat(db): PG indexes/constraints queries + matview UNION in tables (Task 6) * feat(db): get_table_data editability flags + ctid/rowid locator (Task 7) * feat(db): execute_change no-PK locator guard + affected-count check (Task 8) * feat(db): preserve bigint precision as string on PG read path (Task 9) * feat(db): get_indexes / get_constraints commands (Task 10) * feat(store): favorites + recents Store methods (Task 11) * feat(commands): favorites/recents IPC + register indexes/constraints (Task 12) * feat(store): connectionStore favorites/recents/move-selection (Task 13) * feat(lib): recent-connections pure helpers (Task 14) * feat(store): dbViewerStore indexes/constraints + stageCellEdit (Task 15) * feat(grid): pure editability + filter-operator + cell transform (Task 16) * feat(grid): pure keyboard-nav helper (Task 17) * feat(grid): CellEditor inline editor (Task 18) * feat(grid): CellContextMenu + RowDetailDrawer (Task 19) * feat(grid): focus model + keyboard nav + inline edit + copy + context menu (Task 20) * feat(db-viewer): FilterBuilder drag-and-drop + type-aware operators (Task 21) * feat(db-viewer): ObjectExplorer indexes/constraints/procedures + matview icon (Task 22) * feat(home): ConnectionCard favorite star + on-demand StatusDot (Task 23) * feat(home): move-to-folder + recents strip + status wiring (Task 24) * feat(db-viewer): grid wiring + matview read-only + post-commit refetch (Task 25) * docs: v0.5.0 status + roadmap updates (Task 26) * polish: empty/error/loading states for v0.5.0 surfaces (Task 28) * feat(home): duplicateConnection + useConnectionStatus hook, drop StatusDot (FEAT-A) * feat(home): connection card kebab menu — favorite/test/manage (FEAT-B) * fix(home): populate server_version/latency_ms in test_connection + clean online display * style(home): swap grab handle and kebab positions on connection card * style(home): nudge kebab menu to right-1 * style(home): nudge kebab menu to right-0.5 * feat(home): Escape clears + exits focused search * feat(grid): context-menu View/Select Row, outside-click close, Esc cancels edit, FK reference (GRID-A) * feat(grid): smart CellEditor — enum select, FK searchable dropdown, textarea height (GRID-B) * feat(grid): enums + FK options fed into CellEditor (GRID-C) * feat(grid): FK dropdown display-column labels + placeholder + empty state * fix(grid): FK dropdown renders as fixed overlay to avoid clipping * fix(grid): portal FK dropdown to body + FK reference icon instead of click-to-open * style(grid): move FK reference icon to the start of the cell * feat(grid): optimistic staged cell values + pending dot, cleared on refetch * fix(grid): queue is source of truth for staged values — value diff, Clear All clears dots, same-cell edits replace * fix(db-viewer): type getLocator for staged-value matching * test(db-viewer): unit-test deriveStagedValues; fix activeTab null guard * fix(db-viewer): pass table prop to VirtualDataGrid — staged edits now carry the table name * test(db-viewer): use index access instead of .at() for TS lib target * feat(grid): FK dropdown options as one-row column cells (FK-reference style, cap 5) * style(grid): FK dropdown — values only, fixed 360px width, FK-viewer surface styling * style(grid): harden FK dropdown minWidth to 360px * style(grid): cap FK dropdown cells at 3 * style(grid): cap FK dropdown cells at 4 * fix(grid): pending dot clears on commit — values stay until refetch * fix(db): deserialize pg_attribute char columns as i8 — no more panic on get_table_data * docs: reflect grid interactivity, smart editors, optimistic queue, FK reference, kebab status
100 lines
4.5 KiB
TypeScript
100 lines
4.5 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { Plus, Settings as SettingsIcon, FolderPlus, FolderInput, Trash2, Check, X, ChevronDown } from "lucide-react";
|
|
import { Button } from "../ui/Button";
|
|
import { useUiStore } from "../../stores/uiStore";
|
|
import { ImportExportMenu } from "./ImportExportMenu";
|
|
import { TagFilterDropdown } from "../tags/TagFilterDropdown";
|
|
import { DbTypeFilterDropdown } from "./DbTypeFilterDropdown";
|
|
|
|
interface ActionRowProps {
|
|
onImport?: () => void;
|
|
onExport?: () => void;
|
|
onNewFolder?: () => void;
|
|
onFilters?: () => void;
|
|
onDeleteSelected?: () => void;
|
|
onMoveToFolder?: () => void;
|
|
visibleItemIds?: string[];
|
|
}
|
|
|
|
export function ActionRow({ onImport, onExport, onNewFolder, onFilters: _onFilters, onDeleteSelected, onMoveToFolder, visibleItemIds = [] }: ActionRowProps) {
|
|
const setActiveView = useUiStore((s) => s.setActiveView);
|
|
const openSettings = useUiStore((s) => s.openSettings);
|
|
const selectedItemIds = useUiStore((s) => s.selectedItemIds);
|
|
const selectAllItems = useUiStore((s) => s.selectAllItems);
|
|
const clearSelection = useUiStore((s) => s.clearSelection);
|
|
const hasSelection = selectedItemIds.length > 0;
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!menuOpen) return;
|
|
const handler = (e: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
setMenuOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handler);
|
|
return () => document.removeEventListener("mousedown", handler);
|
|
}, [menuOpen]);
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<div className="flex items-center justify-between w-full">
|
|
<h2 className="font-heading text-lg text-text">Saved Connections</h2>
|
|
<div className="flex items-center gap-2">
|
|
<Button onClick={() => setActiveView("new-connection")}>
|
|
<Plus size={14} /> New Connection
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<TagFilterDropdown />
|
|
<DbTypeFilterDropdown />
|
|
<Button variant="ghost" className="text-xs border-0" onClick={onNewFolder ?? (() => {})}>
|
|
<FolderPlus size={14} /> New Folder
|
|
</Button>
|
|
{hasSelection && (
|
|
<Button variant="ghost" className="text-xs border-0" onClick={onMoveToFolder}>
|
|
<FolderInput size={14} /> Move to folder
|
|
</Button>
|
|
)}
|
|
{hasSelection && (
|
|
<div className="relative" ref={menuRef}>
|
|
<Button variant="ghost" className="text-xs border-0" onClick={() => setMenuOpen((o) => !o)}>
|
|
{selectedItemIds.length} selected <ChevronDown size={12} />
|
|
</Button>
|
|
{menuOpen && (
|
|
<div className="absolute left-0 mt-1 rounded-xl bg-surface border border-border py-1 z-10 min-w-[180px] shadow-lg">
|
|
<button
|
|
className="flex items-center gap-2 px-3 py-2 text-sm text-text-muted hover:text-text hover:bg-surface-raised w-full text-left transition-colors cursor-pointer"
|
|
onClick={() => { selectAllItems(visibleItemIds); setMenuOpen(false); }}
|
|
>
|
|
<Check size={14} /> Select All
|
|
</button>
|
|
<button
|
|
className="flex items-center gap-2 px-3 py-2 text-sm text-text-muted hover:text-text hover:bg-surface-raised w-full text-left transition-colors cursor-pointer"
|
|
onClick={() => { clearSelection(); setMenuOpen(false); }}
|
|
>
|
|
<X size={14} /> Clear Selection
|
|
</button>
|
|
<div className="border-t border-border my-1" />
|
|
<button
|
|
className="flex items-center gap-2 px-3 py-2 text-sm !text-red-400 hover:!text-red-300 hover:bg-surface-raised w-full text-left transition-colors cursor-pointer"
|
|
onClick={() => { onDeleteSelected?.(); setMenuOpen(false); }}
|
|
>
|
|
<Trash2 size={14} /> Delete ({selectedItemIds.length})
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 ml-auto">
|
|
<ImportExportMenu onImport={onImport ?? (() => {})} onExport={onExport ?? (() => {})} />
|
|
<Button variant="ghost" className="text-xs" onClick={openSettings}>
|
|
<SettingsIcon size={14} /> Settings
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |