From 86c7616efac5be453325831518cbfcd8ba22b8ef Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sun, 14 Jun 2026 11:49:31 +0700 Subject: [PATCH] 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 --- .../components/layouts/main-sidebar.test.tsx | 62 +++++++++++++++++++ .../src/components/layouts/main-sidebar.tsx | 13 +++- .../layouts/settings-layout.test.tsx | 44 +++++++++++++ .../components/layouts/settings-layout.tsx | 14 +++-- 4 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/layouts/main-sidebar.test.tsx create mode 100644 frontend/src/components/layouts/settings-layout.test.tsx diff --git a/frontend/src/components/layouts/main-sidebar.test.tsx b/frontend/src/components/layouts/main-sidebar.test.tsx new file mode 100644 index 00000000..a9ea1f78 --- /dev/null +++ b/frontend/src/components/layouts/main-sidebar.test.tsx @@ -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 {(location.state as null | { from?: string })?.from ?? 'none'}; +} + +function renderSidebar() { + return render( + + + + + + dashboard} path="/dashboard" /> + } path="/settings" /> + } path="/settings/account" /> + + , + ); +} + +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'); + }); +}); diff --git a/frontend/src/components/layouts/main-sidebar.tsx b/frontend/src/components/layouts/main-sidebar.tsx index 1fb0c0d2..10c7bcf8 100644 --- a/frontend/src/components/layouts/main-sidebar.tsx +++ b/frontend/src/components/layouts/main-sidebar.tsx @@ -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} > - + Settings @@ -354,7 +358,10 @@ export function MainSidebar() { - + Profile diff --git a/frontend/src/components/layouts/settings-layout.test.tsx b/frontend/src/components/layouts/settings-layout.test.tsx new file mode 100644 index 00000000..87736f7a --- /dev/null +++ b/frontend/src/components/layouts/settings-layout.test.tsx @@ -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( + + + } path="/settings"> + account} path="account" /> + providers} path="providers" /> + + + , + ); +} + +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'); + }); +}); diff --git a/frontend/src/components/layouts/settings-layout.tsx b/frontend/src/components/layouts/settings-layout.tsx index 28a34274..ef5f7715 100644 --- a/frontend/src/components/layouts/settings-layout.tsx +++ b/frontend/src/components/layouts/settings-layout.tsx @@ -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 (
- +
@@ -112,7 +118,7 @@ function SettingsLayout() { ); } -function SettingsSidebar() { +function SettingsSidebar({ returnUrl }: { returnUrl: string }) { return ( @@ -143,7 +149,7 @@ function SettingsSidebar() { - + Back to App