FN-7477: clarify desktop remote server setup

Clarifies the desktop Connection Manager so local mode is distinct from adding saved remote servers.

- Split the desktop local server card from the remote server profile flow and keep the editor closed until adding or editing a remote.
- Add desktop-specific remote server copy, accessible labels with profile URLs, and responsive styling for the revised layout.
- Update Connection Manager tests, native shell docs, localized strings, resource types, and the Fusion changeset.

Files changed:
 .changeset/fn-7477-connection-manager-clarity.md   |   7 +
 docs/native-shell.md                               |   5 +-
 .../components/NativeShellConnectionManager.css    |  65 ++++++---
 .../components/NativeShellConnectionManager.tsx    | 153 ++++++++++++--------
 .../NativeShellConnectionManager.test.tsx          | 156 +++++++++++++++------
 packages/i18n/locales/en/app.json                  |  15 +-
 packages/i18n/locales/es/app.json                  |  15 +-
 packages/i18n/locales/fr/app.json                  |  15 +-
 packages/i18n/locales/ko/app.json                  |  15 +-
 packages/i18n/locales/zh-CN/app.json               |  15 +-
 packages/i18n/locales/zh-TW/app.json               |  15 +-
 packages/i18n/src/resources.d.ts                   |  13 +-
 12 files changed, 334 insertions(+), 155 deletions(-)

Fusion-Task-Id: FN-7477

Fusion-Task-Lineage: adc2df09-d713-4ccd-8fb6-20a32c6a553e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-03 17:08:40 -07:00
parent a2b09f277d
commit e8b73623d4
12 changed files with 346 additions and 167 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Clarify the desktop Connection Manager add-remote flow.
category: fix
dev: Desktop Connection Manager now separates Local Server context from saved remote profiles and collapses the remote editor until add/edit.

View File

@@ -57,11 +57,12 @@ Profiles are first-class saved objects shared by onboarding and Connection Manag
Connection Manager supports:
- **Desktop Switch server** shows **Local Server** as a selectable destination in the same list as saved remote/server profiles. Selecting **Local Server** calls `setDesktopMode("local")` and returns the shell to the embedded/local Fusion server without deleting remote profiles.
- **Desktop Switch server** presents the built-in **Local Server** separately from saved **Remote servers**. Local Server is always available in the desktop shell; selecting it calls `setDesktopMode("local")` and returns the shell to the embedded/local Fusion server without deleting remote profiles.
- **Add remote server** is the desktop CTA for saving another Fusion server profile. The remote profile editor stays collapsed until a user chooses **Add remote server** or edits an existing saved profile, so local-only desktop users do not see an empty setup form.
- **Use** (activate a saved remote profile). In desktop local mode, using a remote profile first switches desktop mode back to `remote`, then activates the selected profile.
- **Edit** (update name/URL/token)
- **Delete**
- **Add connection**
- **Mobile Add connection / Scan QR** remains focused on remote profile setup and does not show the desktop-only Local Server guidance.
Activation updates `activeProfileId` and stamps `lastUsedAt` on the selected profile.

View File

@@ -2,21 +2,17 @@
width: min(100%, 42rem);
}
.native-shell-connection-manager__mode-row {
display: flex;
gap: var(--space-sm);
padding: 0 var(--space-xl) var(--space-md);
}
.native-shell-connection-manager__profiles {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: 0 var(--space-xl);
max-height: 16rem;
max-height: 18rem;
overflow: auto;
}
.native-shell-connection-manager__overview,
.native-shell-connection-manager__section-heading,
.native-shell-connection-manager__profile {
display: flex;
align-items: center;
@@ -24,40 +20,56 @@
gap: var(--space-md);
}
.native-shell-connection-manager__destination {
width: 100%;
border-color: var(--border);
color: var(--text);
text-align: start;
cursor: pointer;
}
.native-shell-connection-manager__destination:hover,
.native-shell-connection-manager__destination:focus-visible {
border-color: var(--accent-primary);
}
.native-shell-connection-manager__destination > span:first-child {
.native-shell-connection-manager__overview-copy {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.native-shell-connection-manager__profile-actions {
.native-shell-connection-manager__overview-copy h3,
.native-shell-connection-manager__section-heading h3,
.native-shell-connection-manager__editor h3 {
margin: 0;
}
.native-shell-connection-manager__overview-copy p,
.native-shell-connection-manager__section-heading p {
margin: var(--space-xs) 0 0;
}
.native-shell-connection-manager__section-heading {
padding: var(--space-sm) 0;
}
.native-shell-connection-manager__profile-actions,
.native-shell-connection-manager__mobile-actions {
display: inline-flex;
gap: var(--space-sm);
flex-wrap: wrap;
}
.native-shell-connection-manager__mobile-actions {
padding: var(--space-md) var(--space-xl) 0;
}
.native-shell-connection-manager__editor {
padding-top: var(--space-lg);
}
.native-shell-connection-manager__editor h3 {
padding-bottom: var(--space-sm);
}
.native-shell-connection-manager__empty-state {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.native-shell-connection-manager__empty-state p {
margin: 0;
}
.native-shell-connection-manager__active-pill {
display: inline-flex;
margin-top: var(--space-xs);
@@ -75,13 +87,20 @@
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}
.native-shell-connection-manager__standalone-error {
margin: var(--space-md) var(--space-xl) 0;
}
@media (max-width: 768px) {
.native-shell-connection-manager__overview,
.native-shell-connection-manager__section-heading,
.native-shell-connection-manager__profile {
flex-direction: column;
align-items: flex-start;
}
.native-shell-connection-manager__mode-row {
flex-wrap: wrap;
.native-shell-connection-manager__section-heading > .btn,
.native-shell-connection-manager__overview > .btn {
width: 100%;
}
}

View File

@@ -24,6 +24,7 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
if (!open) return null;
const isAddingConnection = editingProfileId === "__new__";
const isEditorOpen = editingProfileId !== null;
const editingProfile = isAddingConnection
? null
: shellState.profiles.find((profile) => profile.id === editingProfileId) ?? activeProfile;
@@ -37,6 +38,12 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
setError(null);
};
const startNewRemoteDraft = () => {
setEditingProfileId("__new__");
setDraft({ name: "", serverUrl: "", authToken: "" });
setError(null);
};
const saveCurrent = async () => {
setError(null);
try {
@@ -120,44 +127,57 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
<div className="native-shell-connection-manager__profiles">
{isDesktopShell && (
<button
type="button"
className="card native-shell-connection-manager__profile native-shell-connection-manager__destination"
onClick={() => void handleUseLocalServer()}
aria-pressed={isDesktopLocalActive}
>
<span>
<strong>{t("shell.localServerTitle", "Local Server")}</strong>
<span className="settings-muted">{t("shell.localServerDescription", "Use the embedded Fusion server on this device.")}</span>
{isDesktopLocalActive && <span className="native-shell-connection-manager__active-pill">{t("shell.activePill", "Active")}</span>}
</span>
<span className="native-shell-connection-manager__profile-actions" aria-hidden="true">
<span className="btn btn-sm">{isDesktopLocalActive ? t("shell.localServerActive", "Current") : t("shell.use", "Use")}</span>
</span>
</button>
<>
{/*
FNXC:DesktopConnectionManager 2026-07-03-16:25:
Desktop Connection Manager must explain that Local Server is built in and remote servers are saved profiles. Keep the remote editor collapsed until the user explicitly adds or edits a remote server so first-run local mode never looks like incomplete setup.
*/}
<section className="card native-shell-connection-manager__overview" aria-labelledby="native-shell-connection-manager-local-heading">
<div className="native-shell-connection-manager__overview-copy">
<h3 id="native-shell-connection-manager-local-heading">{t("shell.localServerTitle", "Local Server")}</h3>
<p className="settings-muted">{t("shell.localServerDescription", "Use the embedded Fusion server on this device.")}</p>
{isDesktopLocalActive && <span className="native-shell-connection-manager__active-pill">{t("shell.activePill", "Active")}</span>}
</div>
<button
type="button"
className="btn btn-sm"
onClick={() => void handleUseLocalServer()}
aria-label={isDesktopLocalActive ? t("shell.currentLocalServer", "Current Local Server") : t("shell.useLocalServer", "Use Local Server")}
aria-pressed={isDesktopLocalActive}
>
{isDesktopLocalActive ? t("shell.localServerActive", "Current") : t("shell.use", "Use")}
</button>
</section>
<div className="native-shell-connection-manager__section-heading">
<div>
<h3>{t("shell.remoteServersTitle", "Remote servers")}</h3>
<p className="settings-muted">{t("shell.remoteServersDescription", "Save Fusion servers you want this desktop app to open later.")}</p>
</div>
<button type="button" className="btn btn-primary btn-sm" onClick={startNewRemoteDraft}>
{t("shell.addRemoteServer", "Add remote server")}
</button>
</div>
</>
)}
{shellState.profiles.length === 0 ? (
<div className="card native-shell-connection-manager__empty-state">
<p className="settings-muted">{t("shell.noServersSaved", "No remote servers saved yet.")}</p>
<div className="native-shell-connection-manager__profile-actions">
<button
type="button"
className="btn btn-sm"
onClick={() => {
setEditingProfileId("__new__");
setDraft({ name: "", serverUrl: "", authToken: "" });
setError(null);
}}
>
{t("shell.addServer", "Add server")}
</button>
{shellState.host === "mobile-shell" && (
<p className="settings-muted">
{isDesktopShell
? t("shell.noRemoteServersDesktop", "No remote servers saved yet. Add one only when you want this desktop app to open another Fusion server.")
: t("shell.noServersSaved", "No remote servers saved yet.")}
</p>
{!isDesktopShell && (
<div className="native-shell-connection-manager__profile-actions">
<button type="button" className="btn btn-sm" onClick={startNewRemoteDraft}>
{t("shell.addServer", "Add server")}
</button>
<button type="button" className="btn btn-sm" onClick={() => void handleScanQr()}>
{t("shell.scanQr", "Scan QR")}
</button>
)}
</div>
</div>
)}
</div>
) : (
shellState.profiles.map((profile) => (
@@ -171,7 +191,7 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
<button
type="button"
className="btn btn-sm"
aria-label={t("shell.editProfile", "Edit {{name}}", { name: profile.name })}
aria-label={t("shell.editProfile", "Edit {{name}} at {{url}}", { name: profile.name, url: profile.serverUrl })}
onClick={() => {
setEditingProfileId(profile.id);
setDraft(profile);
@@ -179,42 +199,53 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
>
{t("actions.edit", "Edit")}
</button>
<button type="button" className="btn btn-sm" aria-label={t("shell.useProfile", "Use {{name}}", { name: profile.name })} onClick={() => void handleUseProfile(profile.id)}>{t("shell.use", "Use")}</button>
<button type="button" className="btn btn-sm btn-danger" aria-label={t("shell.deleteProfile", "Delete {{name}}", { name: profile.name })} onClick={() => setDeleteCandidate(profile)}>{t("actions.delete", "Delete")}</button>
<button
type="button"
className="btn btn-sm"
aria-label={t("shell.useProfile", "Use {{name}} at {{url}}", { name: profile.name, url: profile.serverUrl })}
onClick={() => void handleUseProfile(profile.id)}
>
{t("shell.use", "Use")}
</button>
<button
type="button"
className="btn btn-sm btn-danger"
aria-label={t("shell.deleteProfile", "Delete {{name}} at {{url}}", { name: profile.name, url: profile.serverUrl })}
onClick={() => setDeleteCandidate(profile)}
>
{t("actions.delete", "Delete")}
</button>
</div>
</div>
))
)}
</div>
<div className="native-shell-connection-manager__mode-row">
<button
type="button"
className="btn"
onClick={() => {
setEditingProfileId("__new__");
setDraft({ name: "", serverUrl: "", authToken: "" });
setError(null);
}}
>
{t("shell.addConnection", "Add connection")}
</button>
{shellState.host === "mobile-shell" && (
{!isDesktopShell && shellState.profiles.length > 0 && (
<div className="native-shell-connection-manager__mobile-actions">
<button type="button" className="btn" onClick={startNewRemoteDraft}>
{t("shell.addConnection", "Add connection")}
</button>
<button type="button" className="btn" onClick={() => void handleScanQr()}>
{t("shell.scanQr", "Scan QR")}
</button>
)}
</div>
</div>
)}
<div className="form-group native-shell-connection-manager__editor">
<label htmlFor="native-shell-connection-manager-name">{t("shell.nameLabel", "Name")}</label>
<input id="native-shell-connection-manager-name" className="input" value={workingName} onChange={(event) => setDraft((value) => ({ ...value, name: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-url">{t("shell.serverUrlLabel", "Server URL")}</label>
<input id="native-shell-connection-manager-url" className="input" value={workingUrl} onChange={(event) => setDraft((value) => ({ ...value, serverUrl: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-token">{t("shell.authTokenLabel", "Auth token (optional)")}</label>
<input id="native-shell-connection-manager-token" className="input" type="password" value={workingToken ?? ""} onChange={(event) => setDraft((value) => ({ ...value, authToken: event.target.value }))} />
{error && <p className="form-error" role="alert">{error}</p>}
</div>
{error && !isEditorOpen && <p className="form-error native-shell-connection-manager__standalone-error" role="alert">{error}</p>}
{isEditorOpen && (
<div className="form-group native-shell-connection-manager__editor">
<h3>{isAddingConnection ? t("shell.addRemoteServer", "Add remote server") : t("shell.editRemoteServer", "Edit remote server")}</h3>
<label htmlFor="native-shell-connection-manager-name">{t("shell.nameLabel", "Name")}</label>
<input id="native-shell-connection-manager-name" className="input" value={workingName} onChange={(event) => setDraft((value) => ({ ...value, name: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-url">{t("shell.serverUrlLabel", "Server URL")}</label>
<input id="native-shell-connection-manager-url" className="input" value={workingUrl} onChange={(event) => setDraft((value) => ({ ...value, serverUrl: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-token">{t("shell.authTokenLabel", "Auth token (optional)")}</label>
<input id="native-shell-connection-manager-token" className="input" type="password" value={workingToken ?? ""} onChange={(event) => setDraft((value) => ({ ...value, authToken: event.target.value }))} />
{error && <p className="form-error" role="alert">{error}</p>}
</div>
)}
{deleteCandidate && (
<div className="native-shell-connection-manager__delete-confirm" role="alertdialog" aria-label={t("shell.deleteConfirmLabel", "Delete server confirmation")}>
@@ -228,8 +259,12 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
<div className="modal-actions">
<button type="button" className="btn" onClick={onClose}>{t("actions.close", "Close")}</button>
<button type="button" className="btn" onClick={resetEditor}>{t("actions.cancel", "Cancel")}</button>
<button type="button" className="btn btn-primary" onClick={() => void saveCurrent()} disabled={!workingUrl.trim()}>{t("actions.save", "Save")}</button>
{isEditorOpen && (
<>
<button type="button" className="btn" onClick={resetEditor}>{t("actions.cancel", "Cancel")}</button>
<button type="button" className="btn btn-primary" onClick={() => void saveCurrent()} disabled={!workingUrl.trim()}>{t("actions.save", "Save")}</button>
</>
)}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { NativeShellConnectionManager } from "../NativeShellConnectionManager";
@@ -6,10 +6,11 @@ function createShellApi() {
return {
getState: vi.fn(),
listProfiles: vi.fn(),
saveProfile: vi.fn(async (input?: { id?: string }) => ({
saveProfile: vi.fn(async (input?: { id?: string; name?: string; serverUrl?: string; authToken?: string | null }) => ({
id: input?.id ?? "p2",
name: "Prod",
serverUrl: "https://fusion.example.com",
name: input?.name || "Prod",
serverUrl: input?.serverUrl || "https://fusion.example.com",
authToken: input?.authToken ?? null,
createdAt: "",
updatedAt: "",
})),
@@ -32,7 +33,37 @@ const remoteProfile = {
};
describe("NativeShellConnectionManager", () => {
it("shows a desktop Local Server destination and switches back from an active remote profile", async () => {
it("explains desktop local mode with no remote profiles and hides the editor until add", () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
open={true}
shellApi={shellApi}
shellState={{ host: "desktop-shell", desktopMode: "local", activeProfileId: null, profiles: [] }}
onClose={vi.fn()}
/>,
);
expect(screen.getByRole("heading", { name: "Local Server" })).toBeInTheDocument();
expect(screen.getByText("Use the embedded Fusion server on this device.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Current Local Server" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("heading", { name: "Remote servers" })).toBeInTheDocument();
expect(screen.getByText(/No remote servers saved yet/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Add remote server" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Add connection" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Local$/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Remote$/i })).not.toBeInTheDocument();
expect(screen.queryByLabelText("Name")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Add remote server" }));
expect(screen.getByRole("heading", { name: "Add remote server" })).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toBeInTheDocument();
expect(screen.getByLabelText("Server URL")).toBeInTheDocument();
expect(screen.getByLabelText("Auth token (optional)")).toHaveAttribute("type", "password");
});
it("keeps the Local Server destination available from desktop remote mode", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
@@ -43,38 +74,42 @@ describe("NativeShellConnectionManager", () => {
/>,
);
expect(screen.getByRole("button", { name: /Local Server/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Local Server" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Use Local Server" })).toHaveAttribute("aria-pressed", "false");
expect(screen.getByText("Remote")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /Local Server/i }));
fireEvent.click(screen.getByRole("button", { name: "Use Local Server" }));
await waitFor(() => expect(shellApi.setDesktopMode).toHaveBeenCalledWith("local"));
expect(shellApi.setActiveProfile).not.toHaveBeenCalled();
});
it("marks Local Server active in desktop local mode and switches remote profile use into remote mode", async () => {
it("separates populated desktop remote profiles from local state and exposes deterministic actions", async () => {
const shellApi = createShellApi();
const duplicateNameProfiles = [
remoteProfile,
{ ...remoteProfile, id: "remote-2", serverUrl: "https://other.example.com" },
];
render(
<NativeShellConnectionManager
open={true}
shellApi={shellApi}
shellState={{
host: "desktop-shell",
desktopMode: "local",
activeProfileId: "remote-1",
profiles: [
remoteProfile,
{ ...remoteProfile, id: "remote-2", serverUrl: "https://other.example.com" },
],
}}
shellState={{ host: "desktop-shell", desktopMode: "local", activeProfileId: "remote-1", profiles: duplicateNameProfiles }}
onClose={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: /Local Server/i })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("Active")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Current Local Server" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "Add remote server" })).toBeInTheDocument();
expect(screen.queryByLabelText("Server URL")).not.toBeInTheDocument();
fireEvent.click(screen.getAllByLabelText("Use Remote")[1]!);
const firstCard = screen.getByText("https://fusion.example.com").closest(".native-shell-connection-manager__profile");
expect(firstCard).not.toBeNull();
expect(within(firstCard as HTMLElement).getByRole("button", { name: "Edit Remote at https://fusion.example.com" })).toBeInTheDocument();
expect(within(firstCard as HTMLElement).getByRole("button", { name: "Use Remote at https://fusion.example.com" })).toBeInTheDocument();
expect(within(firstCard as HTMLElement).getByRole("button", { name: "Delete Remote at https://fusion.example.com" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Use Remote at https://other.example.com" }));
await waitFor(() => {
expect(shellApi.setDesktopMode).toHaveBeenCalledWith("remote");
@@ -83,7 +118,7 @@ describe("NativeShellConnectionManager", () => {
expect(shellApi.setDesktopMode.mock.invocationCallOrder[0]).toBeLessThan(shellApi.setActiveProfile.mock.invocationCallOrder[0]);
});
it("renders Local Server deterministically with no remote profiles and an unset desktop mode", () => {
it("validates and saves a new desktop remote profile only after add is chosen", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
@@ -94,12 +129,54 @@ describe("NativeShellConnectionManager", () => {
/>,
);
expect(screen.getByRole("button", { name: /Local Server/i })).toBeInTheDocument();
expect(screen.getByText("No remote servers saved yet.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Add server" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Add remote server" }));
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "Prod" } });
fireEvent.change(screen.getByLabelText("Server URL"), { target: { value: "ftp://fusion.example.com" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
expect(await screen.findByRole("alert")).toHaveTextContent("Server URL must use http or https");
expect(shellApi.saveProfile).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText("Server URL"), { target: { value: "https://prod.example.com" } });
fireEvent.change(screen.getByLabelText("Auth token (optional)"), { target: { value: "secret-token" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(shellApi.saveProfile).toHaveBeenCalledWith(expect.objectContaining({
id: undefined,
name: "Prod",
serverUrl: "https://prod.example.com",
authToken: "secret-token",
}));
expect(shellApi.setActiveProfile).toHaveBeenCalledWith("p2");
});
expect(screen.queryByText("secret-token")).not.toBeInTheDocument();
});
it("shows active profile indicator and requires delete confirmation", async () => {
it("edits an existing profile from a collapsed desktop editor", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
open={true}
shellApi={shellApi}
shellState={{ host: "desktop-shell", desktopMode: "remote", activeProfileId: "remote-1", profiles: [remoteProfile] }}
onClose={vi.fn()}
/>,
);
expect(screen.queryByLabelText("Server URL")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Edit Remote at https://fusion.example.com" }));
expect(screen.getByRole("heading", { name: "Edit remote server" })).toBeInTheDocument();
fireEvent.change(screen.getByDisplayValue("https://fusion.example.com"), { target: { value: "https://next.example.com" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(shellApi.saveProfile).toHaveBeenCalledWith(expect.objectContaining({ id: "remote-1", serverUrl: "https://next.example.com" }));
expect(shellApi.setActiveProfile).toHaveBeenCalledWith("remote-1");
});
});
it("keeps mobile QR, manual add, active state, and delete confirmation available", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
@@ -110,9 +187,20 @@ describe("NativeShellConnectionManager", () => {
/>,
);
expect(screen.queryByRole("button", { name: /Local Server/i })).toBeNull();
expect(screen.queryByRole("heading", { name: "Local Server" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Add remote server" })).not.toBeInTheDocument();
expect(screen.getByText("Active")).toBeInTheDocument();
fireEvent.click(screen.getByLabelText("Delete Prod"));
expect(screen.getByRole("button", { name: "Add connection" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Scan QR" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Scan QR" }));
await waitFor(() => {
expect(shellApi.startQrScan).toHaveBeenCalled();
expect(screen.getByDisplayValue("https://qr.example.com")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: "Delete Prod at https://fusion.example.com" }));
expect(screen.getByRole("alertdialog", { name: "Delete server confirmation" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Delete" }));
@@ -121,27 +209,7 @@ describe("NativeShellConnectionManager", () => {
});
});
it("edits and saves active profile", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
open={true}
shellApi={shellApi}
shellState={{ host: "mobile-shell", activeProfileId: "p1", profiles: [{ id: "p1", name: "Prod", serverUrl: "https://fusion.example.com", authToken: null, createdAt: "", updatedAt: "" }] }}
onClose={vi.fn()}
/>,
);
fireEvent.change(screen.getByDisplayValue("https://fusion.example.com"), { target: { value: "https://next.example.com" } });
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(shellApi.saveProfile).toHaveBeenCalledWith(expect.objectContaining({ id: "p1", serverUrl: "https://next.example.com" }));
expect(shellApi.setActiveProfile).toHaveBeenCalledWith("p1");
});
});
it("supports empty-state recovery and QR import without a mobile Local Server option", async () => {
it("keeps mobile empty state focused on QR and manual entry without desktop guidance", async () => {
const shellApi = createShellApi();
render(
<NativeShellConnectionManager
@@ -152,13 +220,13 @@ describe("NativeShellConnectionManager", () => {
/>,
);
expect(screen.queryByRole("button", { name: /Local Server/i })).toBeNull();
expect(screen.queryByRole("heading", { name: "Local Server" })).not.toBeInTheDocument();
expect(screen.getByText("No remote servers saved yet.")).toBeInTheDocument();
fireEvent.click(screen.getAllByText("Scan QR")[0]!);
expect(screen.getByRole("button", { name: "Add server" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Scan QR" })).toBeInTheDocument();
expect(screen.queryByLabelText("Server URL")).not.toBeInTheDocument();
await waitFor(() => {
expect(shellApi.startQrScan).toHaveBeenCalled();
expect(screen.getByDisplayValue("https://qr.example.com")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: "Add server" }));
expect(screen.getByLabelText("Server URL")).toBeInTheDocument();
});
});

View File

@@ -6956,8 +6956,8 @@
"defaultProfileName": "Remote Server",
"deleteConfirmLabel": "Delete server confirmation",
"deleteConfirmMessage": "Delete {{name}}? This removes the saved profile.",
"deleteProfile": "Delete {{name}}",
"editProfile": "Edit {{name}}",
"deleteProfile": "Delete {{name}} at {{url}}",
"editProfile": "Edit {{name}} at {{url}}",
"modeLocal": "Local",
"modeRemote": "Remote",
"nameLabel": "Name",
@@ -6966,10 +6966,17 @@
"serverUrlLabel": "Server URL",
"serverUrlProtocolError": "Server URL must use http or https",
"use": "Use",
"useProfile": "Use {{name}}",
"useProfile": "Use {{name}} at {{url}}",
"localServerTitle": "Local Server",
"localServerDescription": "Use the embedded Fusion server on this device.",
"localServerActive": "Current"
"localServerActive": "Current",
"addRemoteServer": "Add remote server",
"editRemoteServer": "Edit remote server",
"remoteServersTitle": "Remote servers",
"remoteServersDescription": "Save Fusion servers you want this desktop app to open later.",
"noRemoteServersDesktop": "No remote servers saved yet. Add one only when you want this desktop app to open another Fusion server.",
"useLocalServer": "Use Local Server",
"currentLocalServer": "Current Local Server"
},
"skills": {
"addSkill": "Add a skill…",

View File

@@ -6946,8 +6946,8 @@
"defaultProfileName": "Servidor remoto",
"deleteConfirmLabel": "Confirmación de eliminación del servidor",
"deleteConfirmMessage": "¿Eliminar {{name}}? Esto eliminará el perfil guardado.",
"deleteProfile": "Eliminar {{name}}",
"editProfile": "Editar {{name}}",
"deleteProfile": "Eliminar {{name}} en {{url}}",
"editProfile": "Editar {{name}} en {{url}}",
"modeLocal": "Local",
"modeRemote": "Remoto",
"nameLabel": "Nombre",
@@ -6956,10 +6956,17 @@
"serverUrlLabel": "URL del servidor",
"serverUrlProtocolError": "La URL del servidor debe usar http o https",
"use": "Usar",
"useProfile": "Usar {{name}}",
"useProfile": "Usar {{name}} en {{url}}",
"localServerTitle": "Servidor local",
"localServerDescription": "Usa el servidor Fusion integrado en este dispositivo.",
"localServerActive": "Actual"
"localServerActive": "Actual",
"addRemoteServer": "Añadir servidor remoto",
"editRemoteServer": "Editar servidor remoto",
"remoteServersTitle": "Servidores remotos",
"remoteServersDescription": "Guarda los servidores Fusion que quieras que esta aplicación de escritorio abra más tarde.",
"noRemoteServersDesktop": "Aún no hay servidores remotos guardados. Añade uno solo cuando quieras que esta aplicación de escritorio abra otro servidor Fusion.",
"useLocalServer": "Usar servidor local",
"currentLocalServer": "Servidor local actual"
},
"skills": {
"addSkill": "Añadir una habilidad…",

View File

@@ -6946,8 +6946,8 @@
"defaultProfileName": "Serveur distant",
"deleteConfirmLabel": "Confirmation de suppression du serveur",
"deleteConfirmMessage": "Supprimer {{name}} ? Cela supprimera le profil enregistré.",
"deleteProfile": "Supprimer {{name}}",
"editProfile": "Modifier {{name}}",
"deleteProfile": "Supprimer {{name}} sur {{url}}",
"editProfile": "Modifier {{name}} sur {{url}}",
"modeLocal": "Local",
"modeRemote": "Distant",
"nameLabel": "Nom",
@@ -6956,10 +6956,17 @@
"serverUrlLabel": "URL du serveur",
"serverUrlProtocolError": "L'URL du serveur doit utiliser http ou https",
"use": "Utiliser",
"useProfile": "Utiliser {{name}}",
"useProfile": "Utiliser {{name}} sur {{url}}",
"localServerTitle": "Serveur local",
"localServerDescription": "Utiliser le serveur Fusion intégré sur cet appareil.",
"localServerActive": "Actuel"
"localServerActive": "Actuel",
"addRemoteServer": "Ajouter un serveur distant",
"editRemoteServer": "Modifier le serveur distant",
"remoteServersTitle": "Serveurs distants",
"remoteServersDescription": "Enregistrez les serveurs Fusion que cette application de bureau doit ouvrir plus tard.",
"noRemoteServersDesktop": "Aucun serveur distant enregistré pour l'instant. Ajoutez-en un uniquement si cette application de bureau doit ouvrir un autre serveur Fusion.",
"useLocalServer": "Utiliser le serveur local",
"currentLocalServer": "Serveur local actuel"
},
"skills": {
"addSkill": "Ajouter une compétence…",

View File

@@ -6946,8 +6946,8 @@
"defaultProfileName": "원격 서버",
"deleteConfirmLabel": "서버 삭제 확인",
"deleteConfirmMessage": "{{name}}을(를) 삭제하시겠습니까? 저장된 프로필이 제거됩니다.",
"deleteProfile": "{{name}} 삭제",
"editProfile": "{{name}} 편집",
"deleteProfile": "{{url}}의 {{name}} 삭제",
"editProfile": "{{url}}의 {{name}} 편집",
"modeLocal": "로컬",
"modeRemote": "원격",
"nameLabel": "이름",
@@ -6956,10 +6956,17 @@
"serverUrlLabel": "서버 URL",
"serverUrlProtocolError": "서버 URL은 http 또는 https를 사용해야 합니다",
"use": "사용",
"useProfile": "{{name}} 사용",
"useProfile": "{{url}}의 {{name}} 사용",
"localServerTitle": "로컬 서버",
"localServerDescription": "이 기기의 내장 Fusion 서버를 사용합니다.",
"localServerActive": "현재"
"localServerActive": "현재",
"addRemoteServer": "원격 서버 추가",
"editRemoteServer": "원격 서버 편집",
"remoteServersTitle": "원격 서버",
"remoteServersDescription": "이 데스크톱 앱에서 나중에 열 Fusion 서버를 저장합니다.",
"noRemoteServersDesktop": "저장된 원격 서버가 없습니다. 이 데스크톱 앱에서 다른 Fusion 서버를 열어야 할 때만 추가하세요.",
"useLocalServer": "로컬 서버 사용",
"currentLocalServer": "현재 로컬 서버"
},
"skills": {
"addSkill": "스킬 추가…",

View File

@@ -6946,8 +6946,8 @@
"defaultProfileName": "远程服务器",
"deleteConfirmLabel": "删除服务器确认",
"deleteConfirmMessage": "删除 {{name}}?这将移除保存的配置文件。",
"deleteProfile": "删除 {{name}}",
"editProfile": "编辑 {{name}}",
"deleteProfile": "删除 {{url}} 上的 {{name}}",
"editProfile": "编辑 {{url}} 上的 {{name}}",
"modeLocal": "本地",
"modeRemote": "远程",
"nameLabel": "名称",
@@ -6956,10 +6956,17 @@
"serverUrlLabel": "服务器 URL",
"serverUrlProtocolError": "服务器 URL 必须使用 http 或 https",
"use": "使用",
"useProfile": "使用 {{name}}",
"useProfile": "使用 {{url}} 上的 {{name}}",
"localServerTitle": "本地服务器",
"localServerDescription": "使用此设备上的内置 Fusion 服务器。",
"localServerActive": "当前"
"localServerActive": "当前",
"addRemoteServer": "添加远程服务器",
"editRemoteServer": "编辑远程服务器",
"remoteServersTitle": "远程服务器",
"remoteServersDescription": "保存你希望此桌面应用稍后打开的 Fusion 服务器。",
"noRemoteServersDesktop": "尚未保存远程服务器。仅当你希望此桌面应用打开另一个 Fusion 服务器时再添加。",
"useLocalServer": "使用本地服务器",
"currentLocalServer": "当前本地服务器"
},
"skills": {
"addSkill": "添加技能…",

View File

@@ -6946,8 +6946,8 @@
"defaultProfileName": "遠端伺服器",
"deleteConfirmLabel": "刪除伺服器確認",
"deleteConfirmMessage": "刪除 {{name}}?這將移除已保存的設定檔。",
"deleteProfile": "刪除 {{name}}",
"editProfile": "編輯 {{name}}",
"deleteProfile": "刪除 {{url}} 上的 {{name}}",
"editProfile": "編輯 {{url}} 上的 {{name}}",
"modeLocal": "本機",
"modeRemote": "遠端",
"nameLabel": "名稱",
@@ -6956,10 +6956,17 @@
"serverUrlLabel": "伺服器 URL",
"serverUrlProtocolError": "伺服器 URL 必須使用 http 或 https",
"use": "使用",
"useProfile": "使用 {{name}}",
"useProfile": "使用 {{url}} 上的 {{name}}",
"localServerTitle": "本機伺服器",
"localServerDescription": "使用此裝置上的內建 Fusion 伺服器。",
"localServerActive": "目前"
"localServerActive": "目前",
"addRemoteServer": "新增遠端伺服器",
"editRemoteServer": "編輯遠端伺服器",
"remoteServersTitle": "遠端伺服器",
"remoteServersDescription": "儲存你希望此桌面應用稍後開啟的 Fusion 伺服器。",
"noRemoteServersDesktop": "尚未儲存遠端伺服器。只有在你希望此桌面應用開啟另一個 Fusion 伺服器時才需要新增。",
"useLocalServer": "使用本機伺服器",
"currentLocalServer": "目前的本機伺服器"
},
"skills": {
"addSkill": "新增技能…",

View File

@@ -6951,27 +6951,34 @@ export default interface Resources {
"shell": {
"activePill": "Active",
"addConnection": "Add connection",
"addRemoteServer": "Add remote server",
"addServer": "Add server",
"authTokenLabel": "Auth token (optional)",
"connectionManager": "Connection Manager",
"connectionManagerLabel": "Connection Manager",
"defaultProfileName": "Remote Server",
"currentLocalServer": "Current Local Server",
"editRemoteServer": "Edit remote server",
"deleteConfirmLabel": "Delete server confirmation",
"deleteConfirmMessage": "Delete {{name}}? This removes the saved profile.",
"deleteProfile": "Delete {{name}}",
"editProfile": "Edit {{name}}",
"deleteProfile": "Delete {{name}} at {{url}}",
"editProfile": "Edit {{name}} at {{url}}",
"localServerActive": "Current",
"localServerDescription": "Use the embedded Fusion server on this device.",
"localServerTitle": "Local Server",
"modeLocal": "Local",
"modeRemote": "Remote",
"nameLabel": "Name",
"noRemoteServersDesktop": "No remote servers saved yet. Add one only when you want this desktop app to open another Fusion server.",
"noServersSaved": "No remote servers saved yet.",
"remoteServersDescription": "Save Fusion servers you want this desktop app to open later.",
"remoteServersTitle": "Remote servers",
"scanQr": "Scan QR",
"serverUrlLabel": "Server URL",
"serverUrlProtocolError": "Server URL must use http or https",
"use": "Use",
"useProfile": "Use {{name}}"
"useLocalServer": "Use Local Server",
"useProfile": "Use {{name}} at {{url}}"
},
"skills": {
"addSkill": "Add a skill…",