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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user