mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-29 06:26:34 +00:00
fix(webui): return "Back to App" to the page the user came from
The settings shell's only exit was a "Back to App" link hardcoded to /flows, so opening Settings or Profile from anywhere dropped the user at the flows list on the way out. - the Settings and Profile entry links pass the current path as location.state.from - SettingsLayout captures it once on entry (surviving sub-tab navigation, which drops state) and points "Back to App" there, falling back to /flows - tests: SettingsLayout honors the origin / falls back / preserves it across sub-tabs; MainSidebar's Settings and Profile links carry the origin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9b9a74bec0
commit
86c7616efa
@@ -0,0 +1,62 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/providers/user-provider', () => ({
|
||||
useUser: () => ({ authInfo: { user: { mail: 'me@example.com', name: 'Test User', type: 'local' } }, logout: vi.fn() }),
|
||||
}));
|
||||
vi.mock('@/hooks/use-theme', () => ({ useTheme: () => ({ setTheme: vi.fn(), theme: 'system' }) }));
|
||||
vi.mock('@/providers/favorites-provider', () => ({
|
||||
useFavorites: () => ({ addFavoriteFlow: vi.fn(), favoriteFlowIds: [], removeFavoriteFlow: vi.fn() }),
|
||||
}));
|
||||
vi.mock('@/providers/sidebar-flows-provider', () => ({ useSidebarFlows: () => ({ flows: [] }) }));
|
||||
vi.mock('@/features/resources/use-resources-upload', () => ({
|
||||
useResourcesUpload: () => ({ fileInputKey: 'k', fileInputProps: {}, openFilePicker: vi.fn() }),
|
||||
}));
|
||||
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
|
||||
import { MainSidebar } from './main-sidebar';
|
||||
|
||||
function FromProbe() {
|
||||
const location = useLocation();
|
||||
|
||||
return <span data-testid="from">{(location.state as null | { from?: string })?.from ?? 'none'}</span>;
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/dashboard']}>
|
||||
<SidebarProvider>
|
||||
<MainSidebar />
|
||||
</SidebarProvider>
|
||||
<Routes>
|
||||
<Route element={<div>dashboard</div>} path="/dashboard" />
|
||||
<Route element={<FromProbe />} path="/settings" />
|
||||
<Route element={<FromProbe />} path="/settings/account" />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('MainSidebar settings entry points', () => {
|
||||
it('the Settings link carries the current path as the return origin', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Settings' }));
|
||||
|
||||
expect(screen.getByTestId('from')).toHaveTextContent('/dashboard');
|
||||
});
|
||||
|
||||
it('the Profile menu item carries the current path as the return origin', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Test User/ }));
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Profile' }));
|
||||
|
||||
expect(screen.getByTestId('from')).toHaveTextContent('/dashboard');
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Link, useMatch, useParams } from 'react-router-dom';
|
||||
import { Link, useLocation, useMatch, useParams } from 'react-router-dom';
|
||||
|
||||
import type { Flow } from '@/providers/sidebar-flows-provider';
|
||||
import type { Theme } from '@/providers/theme-provider';
|
||||
@@ -61,6 +61,7 @@ interface FlowMenuItemProps {
|
||||
}
|
||||
|
||||
export function MainSidebar() {
|
||||
const location = useLocation();
|
||||
const isDashboardActive = useMatch('/dashboard');
|
||||
const isFlowsActive = useMatch('/flows/*');
|
||||
const isTemplatesActive = useMatch('/templates/*');
|
||||
@@ -268,7 +269,10 @@ export function MainSidebar() {
|
||||
asChild
|
||||
isActive={!!isSettingsActive}
|
||||
>
|
||||
<Link to="/settings">
|
||||
<Link
|
||||
state={{ from: location.pathname }}
|
||||
to="/settings"
|
||||
>
|
||||
<Settings />
|
||||
Settings
|
||||
</Link>
|
||||
@@ -354,7 +358,10 @@ export function MainSidebar() {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/account">
|
||||
<Link
|
||||
state={{ from: location.pathname }}
|
||||
to="/settings/account"
|
||||
>
|
||||
<UserIcon className="mr-2 size-4" />
|
||||
Profile
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import SettingsLayout from './settings-layout';
|
||||
|
||||
function renderAt(entry: { pathname: string; state?: unknown }) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[entry]}>
|
||||
<Routes>
|
||||
<Route element={<SettingsLayout />} path="/settings">
|
||||
<Route element={<div>account</div>} path="account" />
|
||||
<Route element={<div>providers</div>} path="providers" />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
const backToApp = () => screen.getByRole('link', { name: /Back to App/ });
|
||||
|
||||
describe('SettingsLayout "Back to App"', () => {
|
||||
it('returns to the page the user came from', () => {
|
||||
renderAt({ pathname: '/settings/account', state: { from: '/dashboard' } });
|
||||
|
||||
expect(backToApp()).toHaveAttribute('href', '/dashboard');
|
||||
});
|
||||
|
||||
it('falls back to /flows when there is no origin', () => {
|
||||
renderAt({ pathname: '/settings/account' });
|
||||
|
||||
expect(backToApp()).toHaveAttribute('href', '/flows');
|
||||
});
|
||||
|
||||
it('keeps the origin after switching settings sub-tabs (which drop location.state)', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAt({ pathname: '/settings/account', state: { from: '/dashboard' } });
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Providers' }));
|
||||
|
||||
expect(backToApp()).toHaveAttribute('href', '/dashboard');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowLeft, FileText, Key, Plug, Settings as SettingsIcon, User } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { NavLink, Outlet, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { getSafeReturnUrl } from '@/lib/utils/auth';
|
||||
|
||||
export interface MenuItem {
|
||||
icon?: React.ReactNode;
|
||||
@@ -97,10 +98,15 @@ function SettingsHeader() {
|
||||
}
|
||||
|
||||
function SettingsLayout() {
|
||||
const location = useLocation();
|
||||
const [returnUrl] = useState(() =>
|
||||
getSafeReturnUrl((location.state as null | { from?: string })?.from ?? null, '/flows'),
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen w-full overflow-hidden">
|
||||
<SettingsSidebar />
|
||||
<SettingsSidebar returnUrl={returnUrl} />
|
||||
<SidebarInset className="flex flex-1 flex-col">
|
||||
<SettingsHeader />
|
||||
<main className="min-h-0 flex-1 overflow-auto p-4">
|
||||
@@ -112,7 +118,7 @@ function SettingsLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsSidebar() {
|
||||
function SettingsSidebar({ returnUrl }: { returnUrl: string }) {
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
@@ -143,7 +149,7 @@ function SettingsSidebar() {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink to="/flows">
|
||||
<NavLink to={returnUrl}>
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to App
|
||||
</NavLink>
|
||||
|
||||
Reference in New Issue
Block a user