From c2e8433fddd472bb6d19d236024e0dda375bd53e Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Fri, 6 Nov 2026 22:28:28 +0800 Subject: [PATCH] fix: allow editing saved hosts with string IDs --- package-lock.json | 80 +++++++++++++++++-- src/frontend/terminal/TerminalTabContent.tsx | 8 +- .../terminal/TerminalTabContent.test.tsx | 49 +++++++++++- 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a4e2c1..6fad27d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "devDependencies": { "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.0", - "@termix-ssh/plugin-sdk": "^1.0.25", + "@termix-ssh/plugin-sdk": "^1.1.0", "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -46,7 +46,7 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@termix-ssh/plugin-sdk": "^1.0.25", + "@termix-ssh/plugin-sdk": "^1.1.0", "drizzle-orm": "^0.45.2", "express": "^5.2.1", "multer": "^2.3.0", @@ -661,6 +661,7 @@ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2256,6 +2257,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -2291,9 +2358,9 @@ } }, "node_modules/@termix-ssh/plugin-sdk": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@termix-ssh/plugin-sdk/-/plugin-sdk-1.0.25.tgz", - "integrity": "sha512-rJlbB/Wsas+0zRQabq4JXErU1dP8S0iYUi0J1LjHeecAirZr0Ffv4QsaagAuHOPXj3XSeYTJQPX0cAkwTR+NkA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@termix-ssh/plugin-sdk/-/plugin-sdk-1.1.1.tgz", + "integrity": "sha512-/bSnsIxvQBmu4qAtPLZH3gjaVSfGggTRRCI227RC/Ay/9UvwIDWMJhbCJeVCGWyG0SIb43fvI6USFi8NxCOPcA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5981,7 +6048,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD", - "optional": true + "optional": true, + "peer": true }, "node_modules/tweetnacl": { "version": "0.14.5", diff --git a/src/frontend/terminal/TerminalTabContent.tsx b/src/frontend/terminal/TerminalTabContent.tsx index 027ef04..10d46dd 100644 --- a/src/frontend/terminal/TerminalTabContent.tsx +++ b/src/frontend/terminal/TerminalTabContent.tsx @@ -66,6 +66,10 @@ export function TerminalTabContent({ }: TabProps) { const tab = tabRecord as unknown as TerminalTabRecord; const host = hostRecord as unknown as Host | undefined; + // Saved shell hosts use string IDs; quick-connect hosts are not editable. + const savedHostId = /^[1-9]\d*$/.test(String(host?.id)) + ? Number(host?.id) + : NaN; // editHost is on the shell from Termix 26.10.6. const editHost = (shell as { editHost?: (hostId: number) => void }).editHost; const { t } = useTranslation(); @@ -151,8 +155,8 @@ export function TerminalTabContent({ : undefined } onEditHost={ - editHost && typeof host?.id === "number" - ? () => editHost(host!.id as number) + editHost && Number.isSafeInteger(savedHostId) + ? () => editHost(savedHostId) : undefined } host={host} diff --git a/tests/frontend/terminal/TerminalTabContent.test.tsx b/tests/frontend/terminal/TerminalTabContent.test.tsx index ea051e3..cd215df 100644 --- a/tests/frontend/terminal/TerminalTabContent.test.tsx +++ b/tests/frontend/terminal/TerminalTabContent.test.tsx @@ -32,7 +32,11 @@ afterEach(() => { captured.props = []; }); -function renderTab(inSplit?: boolean, data?: Record) { +function renderTab( + inSplit?: boolean, + data?: Record, + options: { id?: number | string; editHost?: (id: number) => void } = {}, +) { const props = { tab: { id: "t1", @@ -41,7 +45,7 @@ function renderTab(inSplit?: boolean, data?: Record) { label: "web-01", data, }, - host: { id: "1", name: "web-01", ip: "10.0.0.1", port: 22 }, + host: { id: options.id ?? "1", name: "web-01", ip: "10.0.0.1", port: 22 }, sshHost: { id: 1 }, label: "web-01", isVisible: true, @@ -52,6 +56,7 @@ function renderTab(inSplit?: boolean, data?: Record) { closeTab: vi.fn(), renameTab: vi.fn(), openTab: vi.fn(), + editHost: options.editHost, }, } as unknown as TabProps; render(); @@ -82,3 +87,43 @@ describe("TerminalTabContent in a split", () => { expect(captured.props.at(-1)?.executeCommand).toBeUndefined(); }); }); + +describe("TerminalTabContent host editing", () => { + it.each([7, "7"])( + "opens the existing host editor for saved ID %j", + async (id) => { + const editHost = vi.fn(); + renderTab(undefined, undefined, { id, editHost }); + await waitFor(() => expect(captured.props.length).toBeGreaterThan(0)); + const edit = captured.props.at(-1)?.onEditHost; + expect(edit).toBeTypeOf("function"); + (edit as () => void)(); + expect(editHost).toHaveBeenCalledWith(7); + }, + ); + + it.each([ + "quick-connect-7", + "7suffix", + "", + "1e2", + "0x7", + " 7", + 0, + -7, + 1.5, + Number.MAX_SAFE_INTEGER + 1, + ])("does not offer editing for unsaved or invalid ID %j", async (id) => { + const editHost = vi.fn(); + renderTab(undefined, undefined, { id, editHost }); + await waitFor(() => expect(captured.props.length).toBeGreaterThan(0)); + expect(captured.props.at(-1)?.onEditHost).toBeUndefined(); + expect(editHost).not.toHaveBeenCalled(); + }); + + it("does not offer editing when the shell does not support it", async () => { + renderTab(undefined, undefined, { id: 7 }); + await waitFor(() => expect(captured.props.length).toBeGreaterThan(0)); + expect(captured.props.at(-1)?.onEditHost).toBeUndefined(); + }); +});