Make help easier to find and cloned workspaces open reliably (#2045)
* feat(app): add sidebar help and support menu Give users a single place to run diagnostics, find shortcuts, and reach the support channels. Share one global diagnostic sheet between Settings and the sidebar so both entry points collect the same report. * fix(app): keep cloned workspace navigation atomic A newly merged clone flow still used the previous navigation signature and opened its draft tab separately. Route the cloned workspace and its draft target through the unified navigation command so attention selection cannot replace the intended tab. * test(app): harden sidebar help coverage Accept both the Discord invite URL and its canonical redirect, assert placement through real element geometry, and remove an unnecessary diagnostic-host callback. * test(app): cover support redirect variants Keep the help-menu E2E stable across prerelease versions and the valid redirect forms returned by Discord and GitHub.
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
type EmptyProjectDescriptor as ProjectWithoutWorkspacesDescriptor,
|
||||
type WorkspaceDescriptor,
|
||||
} from "@/stores/session-store";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
|
||||
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
|
||||
|
||||
type OpenProjectPayload = ProjectAddResponse["payload"];
|
||||
@@ -57,8 +59,7 @@ interface WorkspaceOpenCallbacks {
|
||||
isConnected: boolean;
|
||||
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
|
||||
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
|
||||
openDraftTab: (workspaceKey: string) => string | null;
|
||||
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
|
||||
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
|
||||
}
|
||||
|
||||
export interface OpenGithubRepoDirectlyInput extends WorkspaceOpenCallbacks {
|
||||
@@ -122,8 +123,11 @@ function finishWorkspaceOpen(
|
||||
|
||||
input.mergeWorkspaces(normalizedServerId, [workspace]);
|
||||
input.setHasHydratedWorkspaces(normalizedServerId, true);
|
||||
input.openDraftTab(workspaceKey);
|
||||
input.navigateToWorkspace(normalizedServerId, workspace.id);
|
||||
input.navigateToWorkspace({
|
||||
serverId: normalizedServerId,
|
||||
workspaceId: workspace.id,
|
||||
target: { kind: "draft", draftId: generateDraftId() },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
EmptyProjectDescriptor as ProjectWithoutWorkspacesDescriptor,
|
||||
WorkspaceDescriptor,
|
||||
} from "@/stores/session-store";
|
||||
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
|
||||
|
||||
const SERVER_ID = "server-1";
|
||||
const PROJECT_PATH = "/repo/project";
|
||||
@@ -55,15 +56,6 @@ interface RecordedHydrated {
|
||||
hydrated: boolean;
|
||||
}
|
||||
|
||||
interface RecordedOpenDraftTab {
|
||||
workspaceKey: string;
|
||||
}
|
||||
|
||||
interface RecordedNavigate {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
interface RecordedClone {
|
||||
repo: string;
|
||||
targetDirectory: string;
|
||||
@@ -90,23 +82,13 @@ function createFakeSession() {
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeWorkspaceLayout() {
|
||||
const openedTabs: RecordedOpenDraftTab[] = [];
|
||||
return {
|
||||
openedTabs,
|
||||
openDraftTab: (workspaceKey: string) => {
|
||||
openedTabs.push({ workspaceKey });
|
||||
return "tab-1";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeNavigator() {
|
||||
const navigations: RecordedNavigate[] = [];
|
||||
const navigations: NavigateToWorkspaceInput[] = [];
|
||||
return {
|
||||
navigations,
|
||||
navigateToWorkspace: (serverId: string, workspaceId: string) => {
|
||||
navigations.push({ serverId, workspaceId });
|
||||
navigateToWorkspace: (input: NavigateToWorkspaceInput) => {
|
||||
navigations.push(input);
|
||||
return `/hosts/${input.serverId}/workspaces/${input.workspaceId}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -226,7 +208,6 @@ describe("openProjectDirectly", () => {
|
||||
describe("openGithubRepoDirectly", () => {
|
||||
it("opens a cloned GitHub workspace and seeds a draft tab", async () => {
|
||||
const session = createFakeSession();
|
||||
const layout = createFakeWorkspaceLayout();
|
||||
const navigator = createFakeNavigator();
|
||||
const workspacePayload = buildWorkspacePayload();
|
||||
const github = createFakeGithubCloneClient(workspacePayload);
|
||||
@@ -240,7 +221,6 @@ describe("openGithubRepoDirectly", () => {
|
||||
client: github,
|
||||
mergeWorkspaces: session.mergeWorkspaces,
|
||||
setHasHydratedWorkspaces: session.setHasHydratedWorkspaces,
|
||||
openDraftTab: layout.openDraftTab,
|
||||
navigateToWorkspace: navigator.navigateToWorkspace,
|
||||
});
|
||||
|
||||
@@ -261,13 +241,17 @@ describe("openGithubRepoDirectly", () => {
|
||||
workspaceDirectory: PROJECT_PATH,
|
||||
});
|
||||
expect(session.hydrated).toEqual([{ serverId: SERVER_ID, hydrated: true }]);
|
||||
expect(layout.openedTabs).toEqual([{ workspaceKey: `${SERVER_ID}:1` }]);
|
||||
expect(navigator.navigations).toEqual([{ serverId: SERVER_ID, workspaceId: "1" }]);
|
||||
expect(navigator.navigations).toEqual([
|
||||
{
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: "1",
|
||||
target: { kind: "draft", draftId: expect.any(String) },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a workspace without an identity before changing app state", async () => {
|
||||
const session = createFakeSession();
|
||||
const layout = createFakeWorkspaceLayout();
|
||||
const navigator = createFakeNavigator();
|
||||
const github = createFakeGithubCloneClient({ ...buildWorkspacePayload(), id: " " });
|
||||
|
||||
@@ -280,14 +264,12 @@ describe("openGithubRepoDirectly", () => {
|
||||
client: github,
|
||||
mergeWorkspaces: session.mergeWorkspaces,
|
||||
setHasHydratedWorkspaces: session.setHasHydratedWorkspaces,
|
||||
openDraftTab: layout.openDraftTab,
|
||||
navigateToWorkspace: navigator.navigateToWorkspace,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(session.merges).toEqual([]);
|
||||
expect(session.hydrated).toEqual([]);
|
||||
expect(layout.openedTabs).toEqual([]);
|
||||
expect(navigator.navigations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||
import {
|
||||
openGithubRepoDirectly,
|
||||
@@ -61,12 +59,6 @@ export function useOpenGithubRepo(
|
||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
|
||||
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
|
||||
const openDraftTab = useCallback((workspaceKey: string) => {
|
||||
return useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, {
|
||||
kind: "draft",
|
||||
draftId: generateDraftId(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useCallback(
|
||||
async (repo: string, targetDirectory: string, cloneProtocol?: WorkspaceGithubCloneProtocol) => {
|
||||
@@ -79,17 +71,9 @@ export function useOpenGithubRepo(
|
||||
client,
|
||||
mergeWorkspaces,
|
||||
setHasHydratedWorkspaces,
|
||||
openDraftTab,
|
||||
navigateToWorkspace,
|
||||
});
|
||||
},
|
||||
[
|
||||
client,
|
||||
isConnected,
|
||||
mergeWorkspaces,
|
||||
normalizedServerId,
|
||||
openDraftTab,
|
||||
setHasHydratedWorkspaces,
|
||||
],
|
||||
[client, isConnected, mergeWorkspaces, normalizedServerId, setHasHydratedWorkspaces],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user