Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
findPaneByContent,
type SplitLayout,
} from "@/lib/split-layout";
import { usePublishPluginDetailOpener } from "./plugin-detail-navigation";
vi.mock("@/components/ui/app-toast", () => ({
appToast: {
dismiss: vi.fn(),
Expand Down Expand Up @@ -521,6 +522,28 @@ describe("PluginNavSidebarItems", () => {
);
});

it("opens details in the active workspace without changing its route", async () => {
const open = vi.fn(() => true);
function Workspace() {
usePublishPluginDetailOpener(open, true);
return null;
}
render(<Workspace />);
registerPanel("docs", "Docs");
renderSidebarItems({ initialEntry: "/plugins/docs/main" });
fireEvent.pointerDown(
screen.getByRole("button", { name: "Docs panel options" }),
{ button: 0 },
);
fireEvent.click(
await screen.findByRole("menuitem", { name: "View details" }),
);
expect(open).toHaveBeenCalledWith({ pluginId: "docs", title: "Docs" });
expect(screen.getByTestId("location-path").textContent).toBe(
"/plugins/docs/main",
);
});

it.each([
{ compactViewport: false, pluginId: "docs", title: "Docs" },
{ compactViewport: true, pluginId: "docs", title: "Docs" },
Expand Down
8 changes: 8 additions & 0 deletions apps/app/src/components/plugin/PluginNavSidebarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import {
togglePluginNavPanelVisibility,
} from "./pluginNavSidebarOrder";
import { haveSameOrder, reorderStoredOrder } from "@/lib/stored-order";
import { openPluginDetailsInWorkspace } from "./plugin-detail-navigation";

const MORE_TRIGGER_TEST_ID = "sidebar-navigation-more-trigger";

Expand Down Expand Up @@ -1206,6 +1207,13 @@ function PluginNavSidebarItem({
onOpenInSplit={splitEnabled ? openInSplit : undefined}
onOpenDetails={() => {
onNavigate?.();
if (
openPluginDetailsInWorkspace({
pluginId: chrome.pluginId,
title: chrome.title,
})
)
return;
void navigate(getPluginDetailRoutePath({ pluginId: chrome.pluginId }));
}}
onDisable={() => onDisable(row)}
Expand Down
20 changes: 2 additions & 18 deletions apps/app/src/components/plugin/PluginPanelRightPanelHost.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
lazy,
Suspense,
useCallback,
useEffect,
useLayoutEffect,
Expand Down Expand Up @@ -32,7 +30,6 @@ import {
LazyBrowserTabDeck,
LazyHostScopedFilePreviewTabContent,
LazyNewTabPage,
SecondaryPanelContentSkeleton,
LazyThreadSecondaryPanel,
LazyThreadStorageFilePreviewTabContent,
LazyThreadTerminalPanel,
Expand Down Expand Up @@ -98,6 +95,7 @@ import { PluginPanelTabContent } from "./PluginPanelActions";
import { PluginDetailRouteNavigationProvider } from "@/components/ui/app-route-anchor";
import { usePluginCatalogSearch } from "@/hooks/queries/plugin-catalog-queries";
import { usePluginList } from "@/hooks/queries/plugin-settings-queries";
import { PluginDetailTabContent } from "./plugin-detail-navigation";

const TERMINAL_COLS = 100;
const TERMINAL_ROWS = 30;
Expand All @@ -121,27 +119,13 @@ const fixedTabTargetAtomFamily = atomFamily((_targetId: string) =>
atom<FixedTabSessionTarget | null>(null),
);

const LazyPluginDetailPaneView = lazy(() =>
import("@/views/ToolsView").then(({ PluginDetailPaneView }) => ({
default: PluginDetailPaneView,
})),
);

function marketplacePluginDetailTab(pluginId: string) {
return {
id: `${MARKETPLACE_PLUGIN_DETAIL_TAB_PREFIX}${pluginId}`,
kind: "marketplace-plugin-detail" as const,
};
}

function PluginDetailPanelContent({ pluginId }: { pluginId: string }) {
return (
<Suspense fallback={<SecondaryPanelContentSkeleton />}>
<LazyPluginDetailPaneView pluginId={pluginId} />
</Suspense>
);
}

function PluginFixedTabContent({
fixedTabOwnerId,
isOpen,
Expand Down Expand Up @@ -785,7 +769,7 @@ export function PluginPanelRightPanelHost({
revealPanel();
},
renderContent: () => (
<PluginDetailPanelContent pluginId={tabPluginId} />
<PluginDetailTabContent pluginId={tabPluginId} />
),
statusLabel: null,
tab: marketplacePluginDetailTab(tabPluginId),
Expand Down
159 changes: 159 additions & 0 deletions apps/app/src/components/plugin/plugin-detail-navigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// @vitest-environment jsdom
import { act, cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ThreadSecondaryPanelProps } from "@/components/secondary-panel/ThreadSecondaryPanel";
import {
openPluginDetailsInWorkspace,
PluginDetailPanelContext,
usePluginDetailPanelProps,
usePluginDetailPanelState,
} from "./plugin-detail-navigation";

const selectExisting = vi.fn();
const closePanel = vi.fn();
const existingTab = { id: "new-tab:existing", kind: "new-tab" as const };
const baseProps: ThreadSecondaryPanelProps = {
activeTab: existingTab,
canUseGitUi: false,
metadataContent: null,
tabs: [
{
tab: existingTab,
label: "Existing tab",
leadingVisual: null,
statusLabel: null,
renderContent: () => null,
onClose: vi.fn(),
onSelect: selectExisting,
},
],
fixedTabs: [],
isOpen: false,
onTabReorder: vi.fn(),
onPanelFocus: vi.fn(),
onClose: closePanel,
onCollapse: closePanel,
onOpenNewTab: vi.fn(),
isConversationCollapsed: false,
onToggleConversationCollapse: vi.fn(),
renderAsDrawer: false,
};

function PanelProbe({ id }: { id: string }) {
const props = usePluginDetailPanelProps(baseProps);
return (
<div
data-testid={id}
data-active={props.activeTab?.id}
data-open={props.isOpen}
>
{props.tabs.map((tab) => (
<div key={tab.tab.id}>
<button onClick={tab.onSelect}>{tab.label}</button>
<button onClick={tab.onClose}>Close {tab.label}</button>
</div>
))}
<button onClick={props.onClose}>Hide panel</button>
</div>
);
}

function Workspace({
id,
focused,
revision = id,
}: {
id: string;
focused: boolean;
revision?: string;
}) {
const state = usePluginDetailPanelState(revision, focused);
return (
<PluginDetailPanelContext.Provider value={state}>
<PanelProbe id={id} />
</PluginDetailPanelContext.Provider>
);
}

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe("plugin details in the active workspace", () => {
it("opens and focuses a single detail tab without replacing existing tabs", () => {
render(<Workspace id="workspace" focused />);
act(() => {
expect(
openPluginDetailsInWorkspace({ pluginId: "docs", title: "Docs" }),
).toBe(true);
});
expect(screen.getByTestId("workspace").dataset.active).toBe(
"marketplace-plugin:docs",
);
expect(screen.getByTestId("workspace").dataset.open).toBe("true");
act(() => screen.getByRole("button", { name: "Existing tab" }).click());
expect(selectExisting).toHaveBeenCalledOnce();
expect(screen.getByTestId("workspace").dataset.active).toBe(existingTab.id);
act(() =>
openPluginDetailsInWorkspace({ pluginId: "docs", title: "Docs" }),
);
expect(screen.getAllByRole("button", { name: "Docs" })).toHaveLength(1);
act(() => screen.getByRole("button", { name: "Close Docs" }).click());
expect(screen.getByTestId("workspace").dataset.active).toBe(existingTab.id);
expect(screen.getByTestId("workspace").dataset.open).toBe("false");
});

it("targets only the focused pane and unregisters after it unmounts", () => {
const view = render(
<>
<Workspace id="left" focused />
<Workspace id="right" focused={false} />
</>,
);
act(() =>
openPluginDetailsInWorkspace({ pluginId: "docs", title: "Docs" }),
);
expect(screen.getByTestId("left").dataset.active).toBe(
"marketplace-plugin:docs",
);
expect(screen.getByTestId("right").dataset.active).toBe(existingTab.id);
view.rerender(
<>
<Workspace id="left" focused={false} />
<Workspace id="right" focused />
</>,
);
act(() =>
openPluginDetailsInWorkspace({ pluginId: "tasks", title: "Tasks" }),
);
expect(screen.getByTestId("right").dataset.active).toBe(
"marketplace-plugin:tasks",
);
view.unmount();
expect(
openPluginDetailsInWorkspace({ pluginId: "docs", title: "Docs" }),
).toBe(false);
});

it("closes to the adjacent detail tab, then clears when the workspace changes", () => {
const view = render(<Workspace id="workspace" focused />);
act(() =>
openPluginDetailsInWorkspace({ pluginId: "docs", title: "Docs" }),
);
act(() =>
openPluginDetailsInWorkspace({ pluginId: "tasks", title: "Tasks" }),
);
act(() => screen.getByRole("button", { name: "Close Tasks" }).click());
expect(screen.getByTestId("workspace").dataset.active).toBe(
"marketplace-plugin:docs",
);
act(() => screen.getByRole("button", { name: "Hide panel" }).click());
expect(closePanel).toHaveBeenCalledOnce();
expect(screen.getByTestId("workspace").dataset.open).toBe("false");
view.rerender(
<Workspace id="workspace" focused revision="different-workspace" />,
);
expect(screen.queryByRole("button", { name: "Docs" })).toBeNull();
});
});
Loading
Loading