Editor settings, SSH/SSL runtime, data import + table-menu loose ends (#7)

* feat: editor settings model + typed clamped defaults (Task 1)

* feat: carry SSH config + ssh_password to backend DbConfig (Task 2)

* feat: Change enum bulk/drop/empty + type-specific change payload builder (Task 3)

* feat: csv parser + shared export util (Task 4)

* feat: rustls TLS connector factory with modes + client auth (Task 5)

* feat: real SSH tunnel manager (testable backend) + pool eviction hook (Task 6)

* feat: table DDL fetch (sqlite + pg_dump arg builder) (Task 7)

* feat: keychain SSH secrets + connection delete purge + tunnel lifecycle (Task 8)

* feat: real SSH tunnel + TLS connect path for postgres/mysql (Task 9)

* feat: execute_change bulk/drop/empty + get_table_ddl command (Task 10)

* feat: fetch SSH secrets into dbConnect + save on connection form (Task 11)

* feat: Editor settings tab UI (Task 12)

* feat: QueryEditor applies editor settings live (Task 13)

* feat: ImportDialog with CSV/JSON preview + column mapping (Task 14)

* feat: table-menu export/empty/delete/import + queue labels + payload builder (Task 15)

* fix: error sanitization, encrypted-key guard, row-indexed import errors, caps (Task 16)

* docs: mark Editor Settings, SSH/SSL runtime, Data Import, table-menu loose ends shipped (Task 17)

* feat: auto-refresh schema tree after schema-modifying SQL (query + queue drop)

* fix: use theme-consistent red classes for danger menu items (text-error was undefined)

* feat: changes queue as tab-bar popover + amber pending border

* feat: redesign changes popover (visual/SQL toggle, cards, footer actions, Cmd+S)

* refactor: drop per-card status label from changes popover cards

* feat: green completion indicator on committed cards + auto-close tabs of dropped tables

* docs: changes queue popover UX + auto schema refresh statuses
This commit is contained in:
2026-08-02 20:38:23 +08:00
committed by GitHub
parent e32fe7967c
commit e0c0db8352
68 changed files with 3885 additions and 553 deletions
@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { EditorSettingsTab } from "./EditorSettingsTab";
import { useSettingsStore } from "../../stores/settingsStore";
beforeEach(() => {
useSettingsStore.setState({
settings: {
confirm_before_delete: true, default_folder_id: null, theme: "dark", font_size: "medium",
default_ports: {}, tag_order: null, table_refresh_rate: 0, table_page_size: 50,
shortcuts: {}, accent_color: "#2563EB",
editor_font_size: 13, editor_font_family: "Space Mono", editor_word_wrap: "off",
editor_minimap: false, editor_tab_size: 4,
} as any, loading: false, error: null,
});
vi.restoreAllMocks();
});
describe("EditorSettingsTab", () => {
it("renders the five editor option controls", () => {
render(<EditorSettingsTab />);
expect(screen.getByLabelText(/font size/i)).toBeInTheDocument();
expect(screen.getByLabelText(/font family/i)).toBeInTheDocument();
expect(screen.getByLabelText(/word wrap/i)).toBeInTheDocument();
expect(screen.getByRole("switch", { name: /minimap/i })).toBeInTheDocument();
expect(screen.getByLabelText(/tab size/i)).toBeInTheDocument();
});
it("calls updateSetting when word wrap changes", () => {
const update = vi.fn();
useSettingsStore.setState({ updateSetting: update } as any);
render(<EditorSettingsTab />);
fireEvent.change(screen.getByLabelText(/word wrap/i), { target: { value: "on" } });
expect(update).toHaveBeenCalledWith("editor_word_wrap", "on");
});
it("calls updateSetting when minimap toggled", () => {
const update = vi.fn();
useSettingsStore.setState({ updateSetting: update } as any);
render(<EditorSettingsTab />);
fireEvent.click(screen.getByRole("switch", { name: /minimap/i }));
expect(update).toHaveBeenCalledWith("editor_minimap", "true");
});
});
@@ -0,0 +1,44 @@
import { useSettingsStore } from "../../stores/settingsStore";
import { Select } from "../ui/Select";
import { SettingsRow } from "../ui/SettingsRow";
import { Toggle } from "../ui/Toggle";
const FONT_FAMILY_OPTIONS = [
{ value: "Space Mono", label: "Space Mono" },
{ value: "Fira Code", label: "Fira Code" },
{ value: "Menlo", label: "Menlo" },
{ value: "Monaco", label: "Monaco" },
{ value: "Consolas", label: "Consolas" },
{ value: "JetBrains Mono", label: "JetBrains Mono" },
{ value: "monospace", label: "monospace" },
];
const FONT_SIZE_OPTIONS = [8,10,11,12,13,14,16,18,20,24].map((v) => ({ value: String(v), label: String(v) }));
const TAB_SIZE_OPTIONS = [2,4,6,8].map((v) => ({ value: String(v), label: String(v) }));
const WORD_WRAP_OPTIONS = [{ value: "off", label: "Off" }, { value: "on", label: "On" }];
export function EditorSettingsTab() {
const { settings, updateSetting } = useSettingsStore();
if (!settings) return null;
const set = (key: string) => (value: string) => { void updateSetting(key, value); };
return (
<section className="space-y-4">
<SettingsRow title="Font size" description="Editor font size in pixels">
<Select label="Font size" value={String(settings.editor_font_size)} onChange={set("editor_font_size")} options={FONT_SIZE_OPTIONS} />
</SettingsRow>
<SettingsRow title="Font family" description="Monospace font for the SQL editor">
<Select label="Font family" value={settings.editor_font_family} onChange={set("editor_font_family")} options={FONT_FAMILY_OPTIONS} />
</SettingsRow>
<SettingsRow title="Word wrap" description="Wrap long lines in the editor">
<Select label="Word wrap" value={settings.editor_word_wrap} onChange={set("editor_word_wrap")} options={WORD_WRAP_OPTIONS} />
</SettingsRow>
<SettingsRow title="Minimap" description="Show the code minimap">
<Toggle label="Minimap" checked={settings.editor_minimap} onChange={(c) => void updateSetting("editor_minimap", c ? "true" : "false")} />
</SettingsRow>
<SettingsRow title="Tab size" description="Spaces per indentation level">
<Select label="Tab size" value={String(settings.editor_tab_size)} onChange={set("editor_tab_size")} options={TAB_SIZE_OPTIONS} />
</SettingsRow>
</section>
);
}
+16 -2
View File
@@ -33,6 +33,11 @@ vi.mock("../../lib/commands", () => ({
table_page_size: 50,
shortcuts: {},
accent_color: "#2563EB",
editor_font_size: 13,
editor_font_family: "Space Mono",
editor_word_wrap: "off",
editor_minimap: false,
editor_tab_size: 4,
}),
updateSetting: vi.fn().mockResolvedValue(undefined),
getConnections: vi.fn().mockResolvedValue([]),
@@ -68,6 +73,11 @@ const baseSettings = {
table_page_size: 50,
shortcuts: {} as Record<string, string>,
accent_color: "#2563EB",
editor_font_size: 13,
editor_font_family: "Space Mono",
editor_word_wrap: "off" as const,
editor_minimap: false,
editor_tab_size: 4,
};
describe("SettingsPage", () => {
@@ -118,14 +128,18 @@ describe("SettingsPage", () => {
expect(screen.getByRole("tabpanel")).toHaveAttribute("aria-labelledby", "settings-tab-general");
});
it("switches to the Editor tab and shows placeholder", async () => {
it("switches to the Editor tab and shows editor settings", async () => {
const user = userEvent.setup();
render(<SettingsPage />);
await waitFor(() => {
expect(screen.getByRole("tab", { name: /editor/i })).toBeInTheDocument();
});
await user.click(screen.getByRole("tab", { name: /editor/i }));
expect(screen.getByText(/editor settings are coming soon/i)).toBeInTheDocument();
expect(screen.getByLabelText(/font size/i)).toBeInTheDocument();
expect(screen.getByLabelText(/font family/i)).toBeInTheDocument();
expect(screen.getByLabelText(/word wrap/i)).toBeInTheDocument();
expect(screen.getByRole("switch", { name: /minimap/i })).toBeInTheDocument();
expect(screen.getByLabelText(/tab size/i)).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /editor/i })).toHaveAttribute("aria-selected", "true");
});
+2 -8
View File
@@ -6,6 +6,7 @@ import { GeneralSettingsTab } from "./GeneralSettingsTab";
import { TagsSettingsTab } from "./TagsSettingsTab";
import { ShortcutsSettingsTab } from "./ShortcutsSettingsTab";
import { AdvancedSettingsTab } from "./AdvancedSettingsTab";
import { EditorSettingsTab } from "./EditorSettingsTab";
import {
ChevronLeft,
Cog,
@@ -42,14 +43,7 @@ export function SettingsPage() {
load();
}, [load]);
const renderEditor = () => (
<section>
<h2 className="text-sm font-medium text-text mb-3">Editor</h2>
<div className="py-8 text-center text-sm text-text-muted">
Editor settings are coming soon.
</div>
</section>
);
const renderEditor = () => <EditorSettingsTab />;
const renderTabContent = () => {
switch (activeTab) {