Skip to content

Commit 9d81925

Browse files
authored
Merge pull request #1199 from objectstack-ai/claude/refactor-global-sidebar-navigation
2 parents f4186c3 + cf813ad commit 9d81925

File tree

7 files changed

+886
-159
lines changed

7 files changed

+886
-159
lines changed

apps/console/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const HomeLayout = lazy(() => import('./pages/home/HomeLayout').then(m => ({ def
6262

6363
import { ThemeProvider } from './components/theme-provider';
6464
import { ConsoleToaster } from './components/ConsoleToaster';
65+
import { NavigationProvider } from './context/NavigationContext';
6566

6667
/**
6768
* ConnectedShell
@@ -518,6 +519,7 @@ export function App() {
518519
<ConsoleToaster position="bottom-right" />
519520
<ConditionalAuthWrapper authUrl="/api/v1/auth">
520521
<PreviewBanner />
522+
<NavigationProvider>
521523
<BrowserRouter basename={import.meta.env.BASE_URL?.replace(/\/$/, '') || '/'}>
522524
<Suspense fallback={<LoadingScreen />}>
523525
<Routes>
@@ -569,6 +571,7 @@ export function App() {
569571
</Routes>
570572
</Suspense>
571573
</BrowserRouter>
574+
</NavigationProvider>
572575
</ConditionalAuthWrapper>
573576
</ThemeProvider>
574577
);
Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
2-
* Tests for HomeLayout — lightweight nav shell for /home.
3-
* Validates: layout rendering, user avatar, navigation links.
2+
* Tests for HomeLayout — unified sidebar nav shell for /home.
3+
* Validates: layout rendering, sidebar presence, navigation context.
44
*
55
* Note: Radix DropdownMenu portal rendering is limited in jsdom,
66
* so we test the trigger and visible elements rather than dropdown contents.
77
*/
88
import { describe, it, expect, vi, beforeEach } from 'vitest';
9-
import { render, screen, fireEvent } from '@testing-library/react';
9+
import { render, screen } from '@testing-library/react';
1010
import '@testing-library/jest-dom';
1111
import { MemoryRouter } from 'react-router-dom';
1212
import { HomeLayout } from '../pages/home/HomeLayout';
@@ -40,6 +40,9 @@ vi.mock('@object-ui/i18n', () => ({
4040
direction: 'ltr',
4141
i18n: {},
4242
}),
43+
useObjectLabel: () => ({
44+
objectLabel: ({ name, label }: any) => label || name,
45+
}),
4346
}));
4447

4548
// Mock @object-ui/components to keep most components
@@ -51,6 +54,77 @@ vi.mock('@object-ui/components', async (importOriginal) => {
5154
};
5255
});
5356

57+
// Mock @object-ui/layout AppShell
58+
vi.mock('@object-ui/layout', () => ({
59+
AppShell: ({ children, sidebar }: any) => (
60+
<div data-testid="app-shell">
61+
<div data-testid="sidebar">{sidebar}</div>
62+
<div data-testid="content">{children}</div>
63+
</div>
64+
),
65+
useAppShellBranding: () => {},
66+
}));
67+
68+
// Mock NavigationContext
69+
const mockSetContext = vi.fn();
70+
vi.mock('../context/NavigationContext', () => ({
71+
useNavigationContext: () => ({
72+
context: 'home',
73+
setContext: mockSetContext,
74+
currentAppName: undefined,
75+
setCurrentAppName: vi.fn(),
76+
}),
77+
}));
78+
79+
// Mock MetadataProvider
80+
vi.mock('../context/MetadataProvider', () => ({
81+
useMetadata: () => ({
82+
apps: [],
83+
objects: [],
84+
loading: false,
85+
}),
86+
}));
87+
88+
// Mock other required contexts
89+
vi.mock('../context/ExpressionProvider', () => ({
90+
useExpressionContext: () => ({
91+
evaluator: {},
92+
}),
93+
evaluateVisibility: () => true,
94+
}));
95+
96+
vi.mock('@object-ui/permissions', () => ({
97+
usePermissions: () => ({
98+
can: () => true,
99+
}),
100+
}));
101+
102+
vi.mock('../hooks/useRecentItems', () => ({
103+
useRecentItems: () => ({
104+
recentItems: [],
105+
addRecentItem: vi.fn(),
106+
}),
107+
}));
108+
109+
vi.mock('../hooks/useFavorites', () => ({
110+
useFavorites: () => ({
111+
favorites: [],
112+
addFavorite: vi.fn(),
113+
removeFavorite: vi.fn(),
114+
}),
115+
}));
116+
117+
vi.mock('../hooks/useNavPins', () => ({
118+
useNavPins: () => ({
119+
togglePin: vi.fn(),
120+
applyPins: (items: any[]) => items,
121+
}),
122+
}));
123+
124+
vi.mock('../hooks/useResponsiveSidebar', () => ({
125+
useResponsiveSidebar: () => {},
126+
}));
127+
54128
describe('HomeLayout', () => {
55129
beforeEach(() => {
56130
vi.clearAllMocks();
@@ -64,57 +138,20 @@ describe('HomeLayout', () => {
64138
);
65139
};
66140

67-
it('renders the layout shell with data-testid', () => {
141+
it('renders the AppShell with sidebar and content', () => {
68142
renderLayout();
69-
expect(screen.getByTestId('home-layout')).toBeInTheDocument();
143+
expect(screen.getByTestId('app-shell')).toBeInTheDocument();
144+
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
145+
expect(screen.getByTestId('content')).toBeInTheDocument();
70146
});
71147

72-
it('renders the Home branding button in the top bar', () => {
148+
it('sets navigation context to "home" on mount', () => {
73149
renderLayout();
74-
const brand = screen.getByTestId('home-layout-brand');
75-
expect(brand).toBeInTheDocument();
76-
expect(brand).toHaveTextContent('Home');
150+
expect(mockSetContext).toHaveBeenCalledWith('home');
77151
});
78152

79153
it('renders children inside the layout', () => {
80154
renderLayout(<div data-testid="child-content">Hello World</div>);
81155
expect(screen.getByTestId('child-content')).toBeInTheDocument();
82156
});
83-
84-
it('renders user avatar with initials fallback', () => {
85-
renderLayout();
86-
// The avatar trigger should show user initials "AD" (Alice Dev)
87-
expect(screen.getByTestId('home-layout-user-trigger')).toBeInTheDocument();
88-
expect(screen.getByText('AD')).toBeInTheDocument();
89-
});
90-
91-
it('renders Settings button in the top bar', () => {
92-
renderLayout();
93-
expect(screen.getByTestId('home-layout-settings-btn')).toBeInTheDocument();
94-
});
95-
96-
it('navigates to /system when Settings button is clicked', () => {
97-
renderLayout();
98-
fireEvent.click(screen.getByTestId('home-layout-settings-btn'));
99-
expect(mockNavigate).toHaveBeenCalledWith('/system');
100-
});
101-
102-
it('navigates to /home when brand button is clicked', () => {
103-
renderLayout();
104-
fireEvent.click(screen.getByTestId('home-layout-brand'));
105-
expect(mockNavigate).toHaveBeenCalledWith('/home');
106-
});
107-
108-
it('renders sticky header element', () => {
109-
renderLayout();
110-
const header = screen.getByTestId('home-layout').querySelector('header');
111-
expect(header).toBeInTheDocument();
112-
expect(header?.className).toContain('sticky');
113-
});
114-
115-
it('renders user menu trigger as a round button', () => {
116-
renderLayout();
117-
const trigger = screen.getByTestId('home-layout-user-trigger');
118-
expect(trigger.className).toContain('rounded-full');
119-
});
120157
});

apps/console/src/components/ConsoleLayout.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
* ConsoleLayout
33
*
44
* Root layout shell for the console application. Composes the AppShell
5-
* with the sidebar, header, and main content area.
5+
* with the UnifiedSidebar, header, and main content area.
66
* Includes the global floating chatbot (FAB) widget.
7+
* Sets navigation context to 'app' for app-specific routes.
78
* @module
89
*/
910

10-
import React from 'react';
11+
import React, { useEffect } from 'react';
1112
import { AppShell } from '@object-ui/layout';
1213
import { FloatingChatbot, useObjectChat, type ChatMessage } from '@object-ui/plugin-chatbot';
1314
import { useDiscovery } from '@object-ui/react';
14-
import { AppSidebar } from './AppSidebar';
15+
import { UnifiedSidebar } from './UnifiedSidebar';
1516
import { AppHeader } from './AppHeader';
1617
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
18+
import { useNavigationContext } from '../context/NavigationContext';
1719
import { resolveI18nLabel } from '../utils';
1820
import type { ConnectionState } from '../dataSource';
1921

@@ -88,28 +90,35 @@ function ConsoleFloatingChatbot({ appLabel, objects }: { appLabel: string; objec
8890
);
8991
}
9092

91-
export function ConsoleLayout({
92-
children,
93-
activeAppName,
93+
export function ConsoleLayout({
94+
children,
95+
activeAppName,
9496
activeApp,
9597
onAppChange,
9698
objects,
9799
connectionState
98100
}: ConsoleLayoutProps) {
99101
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
100102
const { isAiEnabled } = useDiscovery();
103+
const { setContext, setCurrentAppName } = useNavigationContext();
104+
105+
// Set navigation context to 'app' when this layout mounts
106+
useEffect(() => {
107+
setContext('app');
108+
setCurrentAppName(activeAppName);
109+
}, [setContext, setCurrentAppName, activeAppName]);
101110

102111
return (
103112
<AppShell
104113
sidebar={
105-
<AppSidebar
106-
activeAppName={activeAppName}
107-
onAppChange={onAppChange}
114+
<UnifiedSidebar
115+
activeAppName={activeAppName}
116+
onAppChange={onAppChange}
108117
/>
109118
}
110119
navbar={
111-
<AppHeader
112-
appName={appLabel}
120+
<AppHeader
121+
appName={appLabel}
113122
objects={objects}
114123
connectionState={connectionState}
115124
/>

0 commit comments

Comments
 (0)