From 77b20b1c4b8998d1a424e349fc8d3ca592fda70a Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Tue, 4 Aug 2026 21:28:27 +0800 Subject: [PATCH] fix(ui): connection URI scroll, Tags & Env tab, hide number-input spinners --- .../ConnectionMetadataRow.test.tsx | 42 ++-------- .../connections/ConnectionMetadataRow.tsx | 76 ++----------------- .../DetailedConnectionForm.test.tsx | 4 +- .../connections/DetailedConnectionForm.tsx | 11 +-- src/components/connections/GeneralTab.tsx | 7 +- .../connections/NewConnectionScreen.tsx | 5 +- 6 files changed, 31 insertions(+), 114 deletions(-) diff --git a/src/components/connections/ConnectionMetadataRow.test.tsx b/src/components/connections/ConnectionMetadataRow.test.tsx index 6b906ef..3b92d8f 100644 --- a/src/components/connections/ConnectionMetadataRow.test.tsx +++ b/src/components/connections/ConnectionMetadataRow.test.tsx @@ -1,18 +1,15 @@ import { describe, it, expect, vi } from "vitest"; import { useState } from "react"; -import { render, screen, within } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ConnectionMetadataRow } from "./ConnectionMetadataRow"; import type { ConnectionFormData } from "./connectionFormData"; -import type { Folder, Tag } from "../../lib/types"; const BASE_FORM: ConnectionFormData = { name: "", environment: null, folder_id: null, tag_ids: [], connection_string: "", db_type: "postgresql", host: "", port: 5432, username: null, password: null, database: null, use_keychain: false, ssh_password: null, }; -const folders: Folder[] = [{ id: "f1", name: "Work", parent_id: null, tag_ids: [], created_at: "", updated_at: "" }]; -const tags: Tag[] = [{ id: "t1", name: "prod", color: "#f00", created_at: "" }]; describe("ConnectionMetadataRow", () => { it("renders a Connection Label input and emits name changes", async () => { @@ -23,8 +20,6 @@ describe("ConnectionMetadataRow", () => { return ( { onChange(updates); setForm((prev) => ({ ...prev, ...updates })); @@ -34,37 +29,16 @@ describe("ConnectionMetadataRow", () => { } render(); const label = screen.getByLabelText(/connection label/i); + expect(label).toBeInTheDocument(); await user.type(label, "My DB"); expect(onChange).toHaveBeenLastCalledWith({ name: "My DB" }); }); - it("toggles the tag picker via + Add Tags and selects a tag", async () => { - const user = userEvent.setup(); - const onChange = vi.fn(); - render(); - await user.click(screen.getByRole("button", { name: /add tags/i })); - await user.click(screen.getByRole("button", { name: /prod/i })); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ tag_ids: ["t1"] })); - }); - - it("changes the environment via Set Env", async () => { - const user = userEvent.setup(); - const onChange = vi.fn(); - render(); - await user.click(screen.getByRole("button", { name: /set env/i })); - const envSection = screen.getByTestId("environment-section"); - await user.click(within(envSection).getByRole("button")); - await user.click(within(envSection).getByRole("button", { name: "Production" })); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ environment: "production" })); - }); - - it("changes the folder via the folder select", async () => { - const user = userEvent.setup(); - const onChange = vi.fn(); - render(); - const folderSection = screen.getByTestId("folder-section"); - await user.click(within(folderSection).getByRole("button")); - await user.click(within(folderSection).getByRole("button", { name: "Work" })); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ folder_id: "f1" })); + it("does not render tag/env/folder controls", () => { + render(); + expect(screen.queryByRole("button", { name: /add tags/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /set env/i })).not.toBeInTheDocument(); + expect(screen.queryByTestId("environment-section")).not.toBeInTheDocument(); + expect(screen.queryByTestId("folder-section")).not.toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/src/components/connections/ConnectionMetadataRow.tsx b/src/components/connections/ConnectionMetadataRow.tsx index 4586a30..f51b1dd 100644 --- a/src/components/connections/ConnectionMetadataRow.tsx +++ b/src/components/connections/ConnectionMetadataRow.tsx @@ -1,81 +1,21 @@ -import { useState } from "react"; import { Input } from "../ui/Input"; -import { EnvironmentSelect } from "./EnvironmentSelect"; -import { FolderSelect } from "./FolderSelect"; -import { SearchableTagPicker } from "../tags/SearchableTagPicker"; -import { Plus, Layers } from "lucide-react"; import type { ConnectionFormData } from "./connectionFormData"; -import type { Folder, Tag } from "../../lib/types"; interface ConnectionMetadataRowProps { form: ConnectionFormData; - folders: Folder[]; - tags: Tag[]; onChange: (updates: Partial) => void; } -export function ConnectionMetadataRow({ form, folders, tags, onChange }: ConnectionMetadataRowProps) { - const [openPanel, setOpenPanel] = useState<"tags" | "env" | null>(null); - const toggle = (panel: "tags" | "env") => - setOpenPanel((cur) => (cur === panel ? null : panel)); - +export function ConnectionMetadataRow({ form, onChange }: ConnectionMetadataRowProps) { return (
-
- - onChange({ name: value })} - placeholder="My Production Database" - aria-label="Connection Label" - /> -
- -
- - -
- - {openPanel === "tags" && ( - { - const current = form.tag_ids ?? []; - const next = current.includes(tagId) ? current.filter((id) => id !== tagId) : [...current, tagId]; - onChange({ tag_ids: next }); - }} - /> - )} - {openPanel === "env" && ( -
- - onChange({ environment: value })} /> -
- )} - -
- - onChange({ folder_id: value })} /> -
+ + onChange({ name: value })} + placeholder="My Production Database" + aria-label="Connection Label" + />
); } \ No newline at end of file diff --git a/src/components/connections/DetailedConnectionForm.test.tsx b/src/components/connections/DetailedConnectionForm.test.tsx index 6e40fd0..9e02ec8 100644 --- a/src/components/connections/DetailedConnectionForm.test.tsx +++ b/src/components/connections/DetailedConnectionForm.test.tsx @@ -55,11 +55,11 @@ describe("DetailedConnectionForm", () => { expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ port: 5432 })); }); - it("renders only General and SSH / SSL tabs (no Tags & Env)", () => { + it("renders General, Tags & Env, and SSH / SSL tabs", () => { render(); expect(screen.getByRole("button", { name: /^general$/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /tags & env/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /ssh \/ ssl/i })).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: /tags & env/i })).not.toBeInTheDocument(); }); it("renders the metadata row (Connection Label) above the tabs", () => { diff --git a/src/components/connections/DetailedConnectionForm.tsx b/src/components/connections/DetailedConnectionForm.tsx index c3f656b..53d5f00 100644 --- a/src/components/connections/DetailedConnectionForm.tsx +++ b/src/components/connections/DetailedConnectionForm.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { GeneralTab } from "./GeneralTab"; import { SshSslTab } from "./SshSslTab"; +import { TagsEnvTab } from "./TagsEnvTab"; import { ConnectionMetadataRow } from "./ConnectionMetadataRow"; -import { useConnectionStore } from "../../stores/connectionStore"; import type { ConnectionFormData } from "./connectionFormData"; export interface DetailedConnectionFormProps { @@ -12,20 +12,21 @@ export interface DetailedConnectionFormProps { } export function DetailedConnectionForm({ form, onChange, managedPreset }: DetailedConnectionFormProps) { - const [activeTab, setActiveTab] = useState<"general" | "ssh">("general"); - const folders = useConnectionStore((s) => s.folders); - const tags = useConnectionStore((s) => s.tags); + const [activeTab, setActiveTab] = useState<"general" | "tagsEnv" | "ssh">("general"); return (
- +
+
{activeTab === "general" ? ( + ) : activeTab === "tagsEnv" ? ( + ) : ( } onChange={onChange as (u: Record) => void} /> )} diff --git a/src/components/connections/GeneralTab.tsx b/src/components/connections/GeneralTab.tsx index 7e1c2f7..fb5f4ff 100644 --- a/src/components/connections/GeneralTab.tsx +++ b/src/components/connections/GeneralTab.tsx @@ -21,12 +21,13 @@ export function GeneralTab({ form, onChange, managedPreset }: GeneralTabProps) { {isSqlite ? ( onChange({ host: value })} /> ) : ( - onChange({ connection_string: e.target.value })} placeholder="postgresql://user:password@host:5432/database" aria-label="Connection URI" - className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-2 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors`} + rows={1} + className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-2 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors resize-none overflow-x-auto whitespace-nowrap`} /> )} {managedPreset && ( @@ -50,7 +51,7 @@ export function GeneralTab({ form, onChange, managedPreset }: GeneralTabProps) {
- onChange({ port: value === "" ? null : Number(value) })} placeholder="5432" aria-label="Port" /> + onChange({ port: value === "" ? null : Number(value) })} placeholder="5432" aria-label="Port" className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]" />
diff --git a/src/components/connections/NewConnectionScreen.tsx b/src/components/connections/NewConnectionScreen.tsx index 193662d..c02ba9e 100644 --- a/src/components/connections/NewConnectionScreen.tsx +++ b/src/components/connections/NewConnectionScreen.tsx @@ -294,14 +294,15 @@ export function NewConnectionScreen({ />
) : ( - handleConnectionStringChange(e.target.value) } placeholder="postgresql://user:password@host:5432/database" aria-label={isEntry ? "Connection URI" : undefined} - className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-3 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors ${ + rows={1} + className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-3 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors resize-none overflow-x-auto whitespace-nowrap ${ isEntry ? "" : "sr-only" }`} />