feat(frontend): ensure the planner tab opens when the view button is selected (#12621)

This commit is contained in:
Hiep Le
2026-01-29 13:12:09 +07:00
committed by GitHub
parent 7d1c105b55
commit df47b7b79d
7 changed files with 517 additions and 88 deletions

View File

@@ -1,8 +1,9 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { PlanPreview } from "#/components/features/chat/plan-preview";
import { useConversationStore } from "#/stores/conversation-store";
// Mock the feature flag to always return true (not testing feature flag behavior)
vi.mock("#/utils/feature-flags", () => ({
@@ -20,13 +21,24 @@ vi.mock("react-i18next", async (importOriginal) => {
};
});
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({ conversationId: "test-conversation-id" }),
}));
describe("PlanPreview", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
});
afterEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
it("should render nothing when planContent is null", () => {
@@ -83,39 +95,6 @@ describe("PlanPreview", () => {
expect(container.textContent).toContain("COMMON$READ_MORE");
});
it("should call onViewClick when View button is clicked", async () => {
const user = userEvent.setup();
const onViewClick = vi.fn();
renderWithProviders(
<PlanPreview planContent="Plan content" onViewClick={onViewClick} />,
);
const viewButton = screen.getByTestId("plan-preview-view-button");
expect(viewButton).toBeInTheDocument();
await user.click(viewButton);
expect(onViewClick).toHaveBeenCalledTimes(1);
});
it("should call onViewClick when Read More button is clicked", async () => {
const user = userEvent.setup();
const onViewClick = vi.fn();
const longContent = "A".repeat(350);
renderWithProviders(
<PlanPreview planContent={longContent} onViewClick={onViewClick} />,
);
const readMoreButton = screen.getByTestId("plan-preview-read-more-button");
expect(readMoreButton).toBeInTheDocument();
await user.click(readMoreButton);
expect(onViewClick).toHaveBeenCalledTimes(1);
});
it("should call onBuildClick when Build button is clicked", async () => {
const user = userEvent.setup();
const onBuildClick = vi.fn();
@@ -224,4 +203,68 @@ describe("PlanPreview", () => {
expect(container.querySelector("h5")).toBeInTheDocument();
expect(container.querySelector("h6")).toBeInTheDocument();
});
it("should call selectTab with 'planner' when View button is clicked", async () => {
const user = userEvent.setup();
const planContent = "Plan content";
const conversationId = "test-conversation-id";
// Arrange: Set up initial state
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
renderWithProviders(<PlanPreview planContent={planContent} />);
// Act: Click the View button
const viewButton = screen.getByTestId("plan-preview-view-button");
await user.click(viewButton);
// Assert: Verify selectTab was called with 'planner' and panel was opened
// The hook sets hasRightPanelToggled, which should trigger isRightPanelShown update
// In tests, we need to manually sync or check hasRightPanelToggled
expect(useConversationStore.getState().selectedTab).toBe("planner");
expect(useConversationStore.getState().hasRightPanelToggled).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(`conversation-state-${conversationId}`)!,
);
expect(storedState.selectedTab).toBe("planner");
expect(storedState.rightPanelShown).toBe(true);
});
it("should call selectTab with 'planner' when Read more button is clicked", async () => {
const user = userEvent.setup();
const longContent = "A".repeat(350);
const conversationId = "test-conversation-id";
// Arrange: Set up initial state
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
renderWithProviders(<PlanPreview planContent={longContent} />);
// Act: Click the Read more button
const readMoreButton = screen.getByTestId("plan-preview-read-more-button");
await user.click(readMoreButton);
// Assert: Verify selectTab was called with 'planner' and panel was opened
// The hook sets hasRightPanelToggled, which should trigger isRightPanelShown update
// In tests, we need to manually sync or check hasRightPanelToggled
expect(useConversationStore.getState().selectedTab).toBe("planner");
expect(useConversationStore.getState().hasRightPanelToggled).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(`conversation-state-${conversationId}`)!,
);
expect(storedState.selectedTab).toBe("planner");
expect(storedState.rightPanelShown).toBe(true);
});
});

View File

@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router";
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
import { ConversationTabsContextMenu } from "#/components/features/conversation/conversation-tabs/conversation-tabs-context-menu";
import { useConversationStore } from "#/stores/conversation-store";
const TASK_CONVERSATION_ID = "task-ec03fb2ab8604517b24af632b058c2fd";
const REAL_CONVERSATION_ID = "conv-abc123";
@@ -34,6 +35,11 @@ describe("ConversationTabs localStorage behavior", () => {
localStorage.clear();
vi.resetAllMocks();
mockConversationId = TASK_CONVERSATION_ID;
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
});
describe("task-prefixed conversation IDs", () => {
@@ -57,7 +63,7 @@ describe("ConversationTabs localStorage behavior", () => {
wrapper: createWrapper(REAL_CONVERSATION_ID),
});
const changesTab = screen.getByText("COMMON$CHANGES");
const changesTab = screen.getByTestId("conversation-tab-editor");
await user.click(changesTab);
const consolidatedKey = `conversation-state-${REAL_CONVERSATION_ID}`;
@@ -87,4 +93,102 @@ describe("ConversationTabs localStorage behavior", () => {
expect(parsed.unpinnedTabs).toContain("terminal");
});
});
describe("hook integration", () => {
it("should open panel and select tab when clicking a tab while panel is closed", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
// Arrange: Panel is closed, no tab selected
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
render(<ConversationTabs />, {
wrapper: createWrapper(REAL_CONVERSATION_ID),
});
// Act: Click the terminal tab
const terminalTab = screen.getByTestId("conversation-tab-terminal");
await user.click(terminalTab);
// Assert: Panel should be open and terminal tab selected
expect(useConversationStore.getState().selectedTab).toBe("terminal");
expect(useConversationStore.getState().hasRightPanelToggled).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${REAL_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe("terminal");
expect(storedState.rightPanelShown).toBe(true);
});
it("should close panel when clicking the same active tab", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
render(<ConversationTabs />, {
wrapper: createWrapper(REAL_CONVERSATION_ID),
});
// Act: Click the editor tab again
const editorTab = screen.getByTestId("conversation-tab-editor");
await user.click(editorTab);
// Assert: Panel should be closed
expect(useConversationStore.getState().hasRightPanelToggled).toBe(false);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${REAL_CONVERSATION_ID}`,
)!,
);
expect(storedState.rightPanelShown).toBe(false);
});
it("should switch to different tab when clicking another tab while panel is open", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
render(<ConversationTabs />, {
wrapper: createWrapper(REAL_CONVERSATION_ID),
});
// Act: Click the browser tab
const browserTab = screen.getByTestId("conversation-tab-browser");
await user.click(browserTab);
// Assert: Browser tab should be selected, panel still open
expect(useConversationStore.getState().selectedTab).toBe("browser");
expect(useConversationStore.getState().hasRightPanelToggled).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${REAL_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe("browser");
});
});
});

View File

@@ -0,0 +1,238 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useSelectConversationTab } from "#/hooks/use-select-conversation-tab";
import { useConversationStore } from "#/stores/conversation-store";
const TEST_CONVERSATION_ID = "test-conversation-id";
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({ conversationId: TEST_CONVERSATION_ID }),
}));
describe("useSelectConversationTab", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
});
describe("selectTab", () => {
it("should open panel and select tab when panel is closed", () => {
// Arrange: Panel is closed
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
const { result } = renderHook(() => useSelectConversationTab());
// Act: Select a tab
act(() => {
result.current.selectTab("editor");
});
// Assert: Panel should be open and tab selected
expect(useConversationStore.getState().selectedTab).toBe("editor");
expect(useConversationStore.getState().hasRightPanelToggled).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${TEST_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe("editor");
expect(storedState.rightPanelShown).toBe(true);
});
it("should close panel when clicking the same active tab", () => {
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Act: Click the same tab again
act(() => {
result.current.selectTab("editor");
});
// Assert: Panel should be closed
expect(useConversationStore.getState().hasRightPanelToggled).toBe(false);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${TEST_CONVERSATION_ID}`,
)!,
);
expect(storedState.rightPanelShown).toBe(false);
});
it("should switch to different tab when panel is already open", () => {
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Act: Select a different tab
act(() => {
result.current.selectTab("terminal");
});
// Assert: New tab should be selected, panel still open
expect(useConversationStore.getState().selectedTab).toBe("terminal");
expect(useConversationStore.getState().isRightPanelShown).toBe(true);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${TEST_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe("terminal");
});
});
describe("isTabActive", () => {
it("should return true when tab is selected and panel is visible", () => {
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Assert: Editor tab should be active
expect(result.current.isTabActive("editor")).toBe(true);
});
it("should return false when tab is selected but panel is not visible", () => {
// Arrange: Editor tab selected but panel is closed
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: false,
hasRightPanelToggled: false,
});
const { result } = renderHook(() => useSelectConversationTab());
// Assert: Editor tab should not be active
expect(result.current.isTabActive("editor")).toBe(false);
});
it("should return false when different tab is selected", () => {
// Arrange: Panel is open with editor tab selected
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Assert: Terminal tab should not be active
expect(result.current.isTabActive("terminal")).toBe(false);
});
});
describe("onTabChange", () => {
it("should update both Zustand store and localStorage when changing tab", () => {
// Arrange
useConversationStore.setState({
selectedTab: null,
isRightPanelShown: false,
hasRightPanelToggled: false,
});
const { result } = renderHook(() => useSelectConversationTab());
// Act: Change tab
act(() => {
result.current.onTabChange("browser");
});
// Assert: Both store and localStorage should be updated
expect(useConversationStore.getState().selectedTab).toBe("browser");
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${TEST_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe("browser");
});
it("should set tab to null when passing null", () => {
// Arrange
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Act: Set tab to null
act(() => {
result.current.onTabChange(null);
});
// Assert: Tab should be null
expect(useConversationStore.getState().selectedTab).toBe(null);
// Verify localStorage was updated
const storedState = JSON.parse(
localStorage.getItem(
`conversation-state-${TEST_CONVERSATION_ID}`,
)!,
);
expect(storedState.selectedTab).toBe(null);
});
});
describe("returned values", () => {
it("should return current selectedTab from store", () => {
// Arrange
useConversationStore.setState({
selectedTab: "vscode",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Assert: Should return current selectedTab
expect(result.current.selectedTab).toBe("vscode");
});
it("should return current isRightPanelShown from store", () => {
// Arrange
useConversationStore.setState({
selectedTab: "editor",
isRightPanelShown: true,
hasRightPanelToggled: true,
});
const { result } = renderHook(() => useSelectConversationTab());
// Assert: Should return current panel state
expect(result.current.isRightPanelShown).toBe(true);
});
});
});

View File

@@ -6,6 +6,7 @@ import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
import { useSelectConversationTab } from "#/hooks/use-select-conversation-tab";
import { planHeadings } from "#/components/features/markdown/plan-headings";
const MAX_CONTENT_LENGTH = 300;
@@ -13,20 +14,20 @@ const MAX_CONTENT_LENGTH = 300;
interface PlanPreviewProps {
/** Raw plan content from PLAN.md file */
planContent?: string | null;
onViewClick?: () => void;
onBuildClick?: () => void;
}
/* eslint-disable i18next/no-literal-string */
export function PlanPreview({
planContent,
onViewClick,
onBuildClick,
}: PlanPreviewProps) {
export function PlanPreview({ planContent, onBuildClick }: PlanPreviewProps) {
const { t } = useTranslation();
const { selectTab } = useSelectConversationTab();
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
const handleViewClick = () => {
selectTab("planner");
};
// Truncate plan content for preview
const truncatedContent = useMemo(() => {
if (!planContent) return "";
@@ -49,8 +50,8 @@ export function PlanPreview({
<div className="flex-1" />
<button
type="button"
onClick={onViewClick}
className="flex items-center gap-1 hover:opacity-80 transition-opacity"
onClick={handleViewClick}
className="flex items-center gap-1 hover:opacity-80 transition-opacity cursor-pointer"
data-testid="plan-preview-view-button"
>
<Typography.Text className="font-medium text-[11px] text-white tracking-[0.11px] leading-4">
@@ -73,7 +74,7 @@ export function PlanPreview({
{planContent && planContent.length > MAX_CONTENT_LENGTH && (
<button
type="button"
onClick={onViewClick}
onClick={handleViewClick}
className="text-[#4a67bd] cursor-pointer hover:underline text-left"
data-testid="plan-preview-read-more-button"
>

View File

@@ -2,6 +2,7 @@ import { ComponentType } from "react";
import { cn } from "#/utils/utils";
type ConversationTabNavProps = {
tabValue: string;
icon: ComponentType<{ className: string }>;
onClick(): void;
isActive?: boolean;
@@ -10,6 +11,7 @@ type ConversationTabNavProps = {
};
export function ConversationTabNav({
tabValue,
icon: Icon,
onClick,
isActive,
@@ -22,6 +24,7 @@ export function ConversationTabNav({
onClick={() => {
onClick();
}}
data-testid={`conversation-tab-${tabValue}`}
className={cn(
"flex items-center gap-2 rounded-md cursor-pointer",
"pl-1.5 pr-2 py-1",

View File

@@ -13,38 +13,30 @@ import { ConversationTabNav } from "./conversation-tab-nav";
import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
import { I18nKey } from "#/i18n/declaration";
import { VSCodeTooltipContent } from "./vscode-tooltip-content";
import {
useConversationStore,
type ConversationTab,
} from "#/stores/conversation-store";
import { useConversationStore } from "#/stores/conversation-store";
import { ConversationTabsContextMenu } from "./conversation-tabs-context-menu";
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useSelectConversationTab } from "#/hooks/use-select-conversation-tab";
export function ConversationTabs() {
const { conversationId } = useConversationId();
const {
selectedTab,
isRightPanelShown,
setHasRightPanelToggled,
setSelectedTab,
} = useConversationStore();
const { setHasRightPanelToggled, setSelectedTab } = useConversationStore();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {
state: persistedState,
setSelectedTab: setPersistedSelectedTab,
setRightPanelShown: setPersistedRightPanelShown,
} = useConversationLocalStorageState(conversationId);
const { state: persistedState } =
useConversationLocalStorageState(conversationId);
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
const onTabChange = (value: ConversationTab | null) => {
setSelectedTab(value);
// Persist the selected tab to localStorage
setPersistedSelectedTab(value);
};
const {
selectTab,
isTabActive,
onTabChange,
selectedTab,
isRightPanelShown,
} = useSelectConversationTab();
// Initialize Zustand state from localStorage on component mount
useEffect(() => {
@@ -73,30 +65,12 @@ export function ConversationTabs() {
const { t } = useTranslation();
const onTabSelected = (tab: ConversationTab) => {
if (selectedTab === tab && isRightPanelShown) {
// If clicking the same active tab, close the drawer
setHasRightPanelToggled(false);
setPersistedRightPanelShown(false);
} else {
// If clicking a different tab or drawer is closed, open drawer and select tab
onTabChange(tab);
if (!isRightPanelShown) {
setHasRightPanelToggled(true);
setPersistedRightPanelShown(true);
}
}
};
const isTabActive = (tab: ConversationTab) =>
isRightPanelShown && selectedTab === tab;
const tabs = [
{
tabValue: "editor",
isActive: isTabActive("editor"),
icon: GitChanges,
onClick: () => onTabSelected("editor"),
onClick: () => selectTab("editor"),
tooltipContent: t(I18nKey.COMMON$CHANGES),
tooltipAriaLabel: t(I18nKey.COMMON$CHANGES),
label: t(I18nKey.COMMON$CHANGES),
@@ -105,7 +79,7 @@ export function ConversationTabs() {
tabValue: "vscode",
isActive: isTabActive("vscode"),
icon: VSCodeIcon,
onClick: () => onTabSelected("vscode"),
onClick: () => selectTab("vscode"),
tooltipContent: <VSCodeTooltipContent />,
tooltipAriaLabel: t(I18nKey.COMMON$CODE),
label: t(I18nKey.COMMON$CODE),
@@ -114,7 +88,7 @@ export function ConversationTabs() {
tabValue: "terminal",
isActive: isTabActive("terminal"),
icon: TerminalIcon,
onClick: () => onTabSelected("terminal"),
onClick: () => selectTab("terminal"),
tooltipContent: t(I18nKey.COMMON$TERMINAL),
tooltipAriaLabel: t(I18nKey.COMMON$TERMINAL),
label: t(I18nKey.COMMON$TERMINAL),
@@ -124,7 +98,7 @@ export function ConversationTabs() {
tabValue: "served",
isActive: isTabActive("served"),
icon: ServerIcon,
onClick: () => onTabSelected("served"),
onClick: () => selectTab("served"),
tooltipContent: t(I18nKey.COMMON$APP),
tooltipAriaLabel: t(I18nKey.COMMON$APP),
label: t(I18nKey.COMMON$APP),
@@ -133,7 +107,7 @@ export function ConversationTabs() {
tabValue: "browser",
isActive: isTabActive("browser"),
icon: GlobeIcon,
onClick: () => onTabSelected("browser"),
onClick: () => selectTab("browser"),
tooltipContent: t(I18nKey.COMMON$BROWSER),
tooltipAriaLabel: t(I18nKey.COMMON$BROWSER),
label: t(I18nKey.COMMON$BROWSER),
@@ -145,7 +119,7 @@ export function ConversationTabs() {
tabValue: "planner",
isActive: isTabActive("planner"),
icon: LessonPlanIcon,
onClick: () => onTabSelected("planner"),
onClick: () => selectTab("planner"),
tooltipContent: t(I18nKey.COMMON$PLANNER),
tooltipAriaLabel: t(I18nKey.COMMON$PLANNER),
label: t(I18nKey.COMMON$PLANNER),
@@ -167,6 +141,7 @@ export function ConversationTabs() {
{visibleTabs.map(
(
{
tabValue,
icon,
onClick,
isActive,
@@ -183,6 +158,7 @@ export function ConversationTabs() {
ariaLabel={tooltipAriaLabel}
>
<ConversationTabNav
tabValue={tabValue}
icon={icon}
onClick={onClick}
isActive={isActive}

View File

@@ -0,0 +1,64 @@
import { useConversationLocalStorageState } from "#/utils/conversation-local-storage";
import {
useConversationStore,
type ConversationTab,
} from "#/stores/conversation-store";
import { useConversationId } from "#/hooks/use-conversation-id";
/**
* Custom hook for selecting conversation tabs with consistent behavior.
* Handles panel visibility, state persistence, and tab toggling logic.
*/
export function useSelectConversationTab() {
const { conversationId } = useConversationId();
const {
selectedTab,
isRightPanelShown,
setHasRightPanelToggled,
setSelectedTab,
} = useConversationStore();
const {
setSelectedTab: setPersistedSelectedTab,
setRightPanelShown: setPersistedRightPanelShown,
} = useConversationLocalStorageState(conversationId);
const onTabChange = (value: ConversationTab | null) => {
setSelectedTab(value);
setPersistedSelectedTab(value);
};
/**
* Selects a tab with proper panel visibility handling.
* - If clicking the same active tab while panel is open, closes the panel
* - If clicking a different tab or panel is closed, opens panel and selects tab
*/
const selectTab = (tab: ConversationTab) => {
if (selectedTab === tab && isRightPanelShown) {
// If clicking the same active tab, close the drawer
setHasRightPanelToggled(false);
setPersistedRightPanelShown(false);
} else {
// If clicking a different tab or drawer is closed, open drawer and select tab
onTabChange(tab);
if (!isRightPanelShown) {
setHasRightPanelToggled(true);
setPersistedRightPanelShown(true);
}
}
};
/**
* Checks if a specific tab is currently active (selected and panel is visible).
*/
const isTabActive = (tab: ConversationTab) =>
isRightPanelShown && selectedTab === tab;
return {
selectTab,
isTabActive,
onTabChange,
selectedTab,
isRightPanelShown,
};
}