Skip to content
Merged
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
32 changes: 31 additions & 1 deletion ui-components/layout/CoreNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
currentApp: 'roboledger' | 'roboinvestor' | 'robosystems'
apiUrl?: string
homeHref?: string
/**
* Where the user menu's Documentation item points. Defaults to this app's own
* `/docs`; an app without docs of its own passes an absolute URL. `null` drops
* the item.
*/
docsHref?: string | null
logoAltText?: string
additionalComponents?: React.ReactNode
className?: string
Expand All @@ -42,7 +48,8 @@
apiUrl = process.env.NEXT_PUBLIC_ROBOSYSTEMS_API_URL ||
'http://localhost:8000',
homeHref = '/home',
docsHref = '/docs',
logoAltText,

Check warning on line 52 in ui-components/layout/CoreNavbar.tsx

View workflow job for this annotation

GitHub Actions / test (22)

'logoAltText' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 52 in ui-components/layout/CoreNavbar.tsx

View workflow job for this annotation

GitHub Actions / test (24)

'logoAltText' is defined but never used. Allowed unused args must match /^_/u
additionalComponents,
className = '',
borderColorClass = 'dark:border-gray-700',
Expand Down Expand Up @@ -153,6 +160,7 @@
<UserDropdown
user={user}
apiUrl={apiUrl}
docsHref={docsHref}
onLogout={handleLogout}
/>
</div>
Expand All @@ -167,10 +175,11 @@
interface UserDropdownProps {
user: User | null
apiUrl: string
docsHref: string | null
onLogout: () => void
}

function UserDropdown({ user, apiUrl, onLogout }: UserDropdownProps) {
function UserDropdown({ user, apiUrl, docsHref, onLogout }: UserDropdownProps) {
const settings = useAccountSettingsLink(apiUrl)

return (
Expand Down Expand Up @@ -233,6 +242,27 @@
User Settings
</DropdownItem>
)}
{docsHref && (
// The docs wear the public site's chrome, so they open in a new tab and
// the app keeps its place. This is the app's only global route to them:
// a signed-in visitor to / is sent to /home and never sees the public
// header's links.
<DropdownItem
theme={customTheme.dropdown.floating.item}
as={Link}
href={docsHref}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-between p-3"
>
<span>Documentation</span>
<HiExternalLink
aria-hidden="true"
className="ml-3 h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
/>
<span className="sr-only"> (opens in a new tab)</span>
</DropdownItem>
)}
<DropdownDivider
theme={{ divider: customTheme.dropdown.floating.divider }}
/>
Expand Down
77 changes: 77 additions & 0 deletions ui-components/layout/__tests__/CoreNavbar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'

vi.mock('next/navigation', () => ({
usePathname: () => '/home',
}))

vi.mock('../../../auth-components', () => ({
AppSwitcher: () => <span />,
}))

vi.mock('../../../auth-components/AuthProvider', () => ({
useAuth: () => ({ logout: vi.fn() }),
}))

vi.mock('../../../contexts', () => ({
useSidebarContext: () => ({
desktop: { isCollapsed: false, toggle: vi.fn() },
mobile: { isOpen: false, toggle: vi.fn(), close: vi.fn() },
}),
}))

vi.mock('../../../hooks', () => ({
useMediaQuery: () => true,
useUser: () => ({
user: { name: 'Demo User', email: 'demo@example.com' },
}),
}))

vi.mock('../account-settings', () => ({
useAccountSettingsLink: () => ({
isCrossApp: false,
href: '/settings',
isOpening: false,
description: 'Account settings',
}),
}))

import { CoreNavbar } from '../CoreNavbar'

const props = { appName: 'RoboSystems', currentApp: 'robosystems' as const }

// Flowbite mounts a dropdown's items only once it is open.
function openUserMenu() {
fireEvent.click(screen.getByRole('button', { name: 'User menu' }))
}

describe('CoreNavbar user menu', () => {
it('offers Documentation on this app by default, in a new tab', () => {
render(<CoreNavbar {...props} />)
openUserMenu()
const docs = screen.getByRole('link', { name: /Documentation/ })

expect(docs.getAttribute('href')).toBe('/docs')
expect(docs.getAttribute('target')).toBe('_blank')
expect(docs.getAttribute('rel')).toBe('noopener noreferrer')
})

it('takes an absolute href for an app with no docs of its own', () => {
render(
<CoreNavbar {...props} docsHref="https://robosystems.ai/docs/guides" />
)
openUserMenu()

expect(
screen.getByRole('link', { name: /Documentation/ }).getAttribute('href')
).toBe('https://robosystems.ai/docs/guides')
})

it('drops the item when an app passes null', () => {
render(<CoreNavbar {...props} docsHref={null} />)
openUserMenu()

expect(screen.queryByRole('link', { name: /Documentation/ })).toBeNull()
expect(screen.getByText('Sign out')).toBeDefined()
})
})
Loading