mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: merge host editor terminal tabs into one owned by ssh-terminal
This commit is contained in:
1 parent
fff373d5bd
commit
c7fd900732
10 files changed
+162
-986
No files matched your search
@@ -220,5 +220,8 @@
|
||||
"title": "Read command history",
|
||||
"description": "Lets other features, such as the AI assistant, read your command history."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"tabTerminal": "Terminal"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lazy, Suspense, type ComponentType } from "react";
|
||||
import { History, Laptop, Terminal } from "lucide-react";
|
||||
import { History, Laptop, SquareTerminal, Terminal } from "lucide-react";
|
||||
import type {
|
||||
PanelProps,
|
||||
StandaloneViewProps,
|
||||
@@ -21,6 +21,7 @@ import { TerminalView } from "./TerminalView";
|
||||
import { HistoryPanel } from "./history/HistoryPanel";
|
||||
import { TouchInputSettings } from "./settings/TouchInputSettings";
|
||||
import { ImageStorageTest } from "./settings/ImageStorageTest";
|
||||
import { HostTerminalSection } from "./settings/HostTerminalSection";
|
||||
import { resetTouchInputSettingsCache } from "./terminal/touch-input-settings-store";
|
||||
import { hostSetting } from "./terminal-api";
|
||||
|
||||
@@ -94,6 +95,15 @@ export function activate(app: TermixApp): void {
|
||||
!!host.enableSsh && hostSetting(host, "enableTerminal", true),
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "terminal",
|
||||
group: "ssh",
|
||||
titleKey: "hosts.tabTerminal",
|
||||
icon: SquareTerminal,
|
||||
order: 10,
|
||||
component: HostTerminalSection,
|
||||
});
|
||||
|
||||
if (isElectron()) {
|
||||
app.registerTab("local-terminal", LocalTerminalTab, {
|
||||
icon: Laptop,
|
||||
|
||||
@@ -63,6 +63,11 @@ describe(`${manifest.id} activate`, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("owns the host editor's Terminal tab in the SSH group", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
expect(rendered.registered.hostEditorSections()).toEqual(["terminal"]);
|
||||
});
|
||||
|
||||
it("registers the command history panel and its rail item", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
expect(rendered.registered.panels()).toContain("history");
|
||||
|
||||
@@ -344,7 +344,6 @@
|
||||
"uploadFile": "Upload File",
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabStats": "Host Metrics",
|
||||
"tabSharing": "Sharing",
|
||||
"tabAuthentication": "Authentication",
|
||||
|
||||
@@ -70,6 +70,12 @@ export { useTabs, useTabsSafe } from "@/shell/TabContext";
|
||||
// Slots: a plugin can offer places for other plugins to fill.
|
||||
export { ActionSlot, ComponentSlot } from "@/shell/ActionSlot";
|
||||
|
||||
// A plugin's manifest host settings, drawn inside its own host editor section.
|
||||
export {
|
||||
HostFeatureFields,
|
||||
type HostFeatureFieldsProps,
|
||||
} from "@/settings/HostPluginSections";
|
||||
|
||||
// Components other plugins offer by id (app.registerComponent), rendered with
|
||||
// a fallback while their plugin is off.
|
||||
export { PluginComponent } from "@/plugin-host/component-registry";
|
||||
@@ -103,6 +109,11 @@ export * from "@/lib/terminal-themes";
|
||||
export { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
|
||||
export { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
|
||||
export * from "@/lib/terminal-look/terminal-font-zoom";
|
||||
export { TerminalPreview } from "@/components/terminal-preview/TerminalPreview";
|
||||
export {
|
||||
HostTerminalSettings,
|
||||
type HostTerminalSettingsProps,
|
||||
} from "@/sidebar/HostTerminalSettings";
|
||||
export { copyToClipboard, readFromClipboard } from "@/lib/clipboard";
|
||||
export { RobustClipboardProvider } from "@/lib/clipboard-provider";
|
||||
export { useTheme as useAppTheme } from "@/components/theme-provider";
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { SectionCard } from "@/components/section-card";
|
||||
import { PluginIcon } from "@/lib/plugin-icon";
|
||||
import { getPlugins, type PluginSummary } from "@/api/plugins-api";
|
||||
import { usePluginScope } from "@/plugin-host/scope";
|
||||
import { SettingsFieldRow } from "./SettingsFields";
|
||||
import { hasVisibleFields, isFieldActive } from "./settings-fields-util";
|
||||
|
||||
@@ -112,6 +113,47 @@ export function HostPluginSections({
|
||||
);
|
||||
}
|
||||
|
||||
export interface HostFeatureFieldsProps {
|
||||
// The host editor form, as handed to a host editor section.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
form: any;
|
||||
updateForm: (
|
||||
patch: (form: Record<string, unknown>) => Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The calling plugin's manifest host settings, for a plugin that registered
|
||||
* its own host editor section and so gets no generated tab.
|
||||
*/
|
||||
export function HostFeatureFields({
|
||||
form,
|
||||
updateForm,
|
||||
}: HostFeatureFieldsProps) {
|
||||
const pluginId = usePluginScope();
|
||||
const plugin = usePluginHostSections().find((p) => p.id === pluginId);
|
||||
if (!plugin) return null;
|
||||
const values = (form?.pluginSettings as HostPluginSettings | undefined) ?? {};
|
||||
return (
|
||||
<HostPluginSection
|
||||
plugin={plugin}
|
||||
values={values[plugin.id] ?? {}}
|
||||
setValue={(key, value) =>
|
||||
updateForm((current) => {
|
||||
const all = (current.pluginSettings ?? {}) as HostPluginSettings;
|
||||
return {
|
||||
...current,
|
||||
pluginSettings: {
|
||||
...all,
|
||||
[plugin.id]: { ...(all[plugin.id] ?? {}), [key]: value },
|
||||
},
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HostPluginSection({
|
||||
plugin,
|
||||
values,
|
||||
|
||||
+29
-968
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { useEffect, useRef, type ComponentType, type ReactNode } from "react";
|
||||
import { KeyRound, Settings, SquareTerminal, Terminal } from "lucide-react";
|
||||
import { KeyRound, Settings, Terminal } from "lucide-react";
|
||||
import { byOrderThenId, createRegistry } from "@/lib/registry";
|
||||
|
||||
/** Core host editor tabs. Plugins add theirs through registerHostEditorSection. */
|
||||
export type CoreHostTabId = "general" | "ssh" | "terminal-options";
|
||||
export type CoreHostTabId = "general" | "ssh";
|
||||
export type HostTabId = CoreHostTabId | (string & {});
|
||||
export type CredentialTabId = "general" | "auth";
|
||||
|
||||
@@ -62,7 +62,7 @@ export const hostEditorSectionList = sections.list;
|
||||
export const useHostEditorSections = sections.useList;
|
||||
export const resetHostEditorSections = sections.reset;
|
||||
|
||||
const CORE_SSH_GROUP = new Set<string>(["ssh", "terminal-options"]);
|
||||
const CORE_SSH_GROUP = new Set<string>(["ssh"]);
|
||||
|
||||
/** Whether a tab lives in the SSH group's second strip. */
|
||||
export function isSshGroupTab(id: string): boolean {
|
||||
@@ -149,12 +149,6 @@ export function makeHostSshSubTabs(
|
||||
icon: <Settings className="size-3" />,
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
id: "terminal-options",
|
||||
label: t("hosts.tabTerminal"),
|
||||
icon: <SquareTerminal className="size-3" />,
|
||||
order: 10,
|
||||
},
|
||||
],
|
||||
"ssh",
|
||||
t,
|
||||
|
||||
@@ -244,11 +244,7 @@ describe("registries through the app object", () => {
|
||||
it("adds a host editor section to the SSH group in order", async () => {
|
||||
await mount();
|
||||
const ids = makeHostSshSubTabs((key) => key).map((tab) => tab.id);
|
||||
expect(ids.slice(0, 3)).toEqual([
|
||||
"ssh",
|
||||
"terminal-options",
|
||||
"fixture-section",
|
||||
]);
|
||||
expect(ids.slice(0, 2)).toEqual(["ssh", "fixture-section"]);
|
||||
expect(makeHostTabs((key) => key).map((tab) => tab.id)).not.toContain(
|
||||
"fixture-section",
|
||||
);
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import type { PluginSummary } from "@/api/plugins-api";
|
||||
import {
|
||||
hostFeatureTabId,
|
||||
@@ -14,7 +20,16 @@ import {
|
||||
registerHostEditorSection,
|
||||
resetHostEditorSections,
|
||||
} from "@/sidebar/HostManagerTabs";
|
||||
import { HostPluginSections } from "@/settings/HostPluginSections";
|
||||
import {
|
||||
HostFeatureFields,
|
||||
HostPluginSections,
|
||||
} from "@/settings/HostPluginSections";
|
||||
import { withPluginScope } from "@/plugin-host/scope";
|
||||
|
||||
const getPlugins = vi.fn();
|
||||
vi.mock("@/api/plugins-api", () => ({
|
||||
getPlugins: () => getPlugins(),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
@@ -266,3 +281,43 @@ describe("HostPluginSections", () => {
|
||||
expect(screen.getByDisplayValue("web")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HostFeatureFields", () => {
|
||||
it("draws the calling plugin's fields and writes into its pluginSettings", async () => {
|
||||
getPlugins.mockResolvedValue([
|
||||
plugin(),
|
||||
plugin({ id: "warpgate", name: "Warpgate" }),
|
||||
]);
|
||||
const form = {
|
||||
pluginSettings: {
|
||||
docker: { enableDocker: false },
|
||||
warpgate: { enableDocker: true },
|
||||
},
|
||||
};
|
||||
const updateForm = vi.fn();
|
||||
const Scoped = withPluginScope("docker", HostFeatureFields);
|
||||
render(<Scoped form={form} updateForm={updateForm} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("docker:enableDocker.label")).toBeTruthy(),
|
||||
);
|
||||
expect(screen.getAllByText("Docker")).toHaveLength(1);
|
||||
expect(screen.queryByText("Warpgate")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getAllByRole("button")[0]);
|
||||
const patch = updateForm.mock.calls[0][0];
|
||||
expect(patch(form).pluginSettings).toEqual({
|
||||
docker: { enableDocker: true },
|
||||
warpgate: { enableDocker: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders nothing outside a plugin", async () => {
|
||||
getPlugins.mockResolvedValue([plugin()]);
|
||||
const { container } = render(
|
||||
<HostFeatureFields form={{}} updateForm={vi.fn()} />,
|
||||
);
|
||||
await waitFor(() => expect(getPlugins).toHaveBeenCalled());
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user