refactor AppSidebarV2

This commit is contained in:
Francesco Sansalvadore committed 2026-03-27 22:36:43 +01:00
1 parent 8892ffbebe
commit 19ebfe6bc7
5 files changed
+451 -270

No files matched your search

@@ -1,283 +1,41 @@
import { useFlag, useIsMFAEnabled, useParams } from 'common'
import {
useIsColumnLevelPrivilegesEnabled,
useUnifiedLogsPreview,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Connect } from 'components/interfaces/Connect/Connect'
import { ConnectSheet } from 'components/interfaces/ConnectSheet/ConnectSheet'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { usePHFlag } from 'hooks/ui/useFlag'
import { Home } from 'icons'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import {
Blocks,
Boxes,
ChartArea,
Compass,
Plug,
Receipt,
Search,
Settings,
Users,
} from 'lucide-react'
import { useRouter } from 'next/router'
import { IS_PLATFORM } from 'lib/constants'
import { Plug, Search } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { Button, Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { OrgSelector } from '../Navigation/NavigationBar/OrgSelector'
import { ProjectBranchSelector } from '../Navigation/NavigationBar/ProjectBranchSelector'
import { useGenerateSettingsMenu } from '../ProjectSettingsLayout/SettingsMenu.utils'
import { NavGroup } from './NavGroup'
import {
generateDatabaseNavItems,
generateObservabilityNavItems,
generatePlatformNavItems,
} from './NavigationV2.utils'
import { NavUser } from './NavUser'
import { type AppSidebarScope, useAppSidebarNavItems } from './useAppSidebarNavItems'
export type AppSidebarScope = 'project' | 'organization'
export type { AppSidebarScope }
interface AppSidebarV2Props {
scope?: AppSidebarScope
}
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
const router = useRouter()
const { ref: projectRef, slug } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const setCommandMenuOpen = useSetCommandMenuOpen()
const isUserMFAEnabled = useIsMFAEnabled()
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const connectSheetFlag = usePHFlag<string | boolean>('connectSheet')
const isFlagResolved = connectSheetFlag !== undefined
const isConnectSheetEnabled = connectSheetFlag === true || connectSheetFlag === 'variation'
const currentScope =
scope ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
const isProjectScope = currentScope === 'project'
const ref = projectRef ?? project?.ref ?? ''
const organizationSlug = slug ?? (router.query.orgSlug as string | undefined) ?? ''
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
const settingsMenu = useGenerateSettingsMenu()
// Database flags
const { data: extensions } = useDatabaseExtensionsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
enabled: isProjectScope,
}
)
const { data: addons } = useProjectAddonsQuery(
{ projectRef: project?.ref },
{ enabled: isProjectScope }
)
const pgNetExtensionExists = (extensions ?? []).find((ext) => ext.name === 'pg_net') !== undefined
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
const enablePgReplicate = useIsETLPrivateAlpha()
const {
databaseReplication: showPgReplicate,
databaseRoles: showRoles,
integrationsWrappers: showWrappers,
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
// Platform flags
const {
projectAuthAll: authEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_edge_function:all',
'project_storage:all',
'realtime:all',
])
const authOverviewPageEnabled = useFlag('authOverviewPage')
// Other flags
const showReports = useIsFeatureEnabled('reports:all')
const showBilling = useIsFeatureEnabled('billing:all')
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
const disableAccessMfa =
selectedOrganization?.organization_requires_mfa === true && !isUserMFAEnabled
const buildOrgHref = (href: string) => (disableAccessMfa ? router.asPath : href)
const projectSettingsItems =
isProjectScope && ref
? settingsMenu
.flatMap((group) => {
if (group.title === 'Billing' && !selectedOrganization?.slug) return []
return group.items
})
.filter((item) => !item.disabled)
.map((item) => ({
title: item.name,
url: item.url,
}))
: []
const isProjectHome =
router.pathname === `/project/[ref]` || router.pathname === `/project/${ref}`
const isProjectSettings = router.pathname.includes('/settings')
const projectItems = isProjectScope
? [
{
title: 'Home',
url: ref ? `/project/${ref}` : '/project',
icon: Home,
isActive: isProjectHome,
},
{
title: 'Project Settings',
url:
ref && IS_PLATFORM
? `/project/${ref}/settings/general`
: ref
? `/project/${ref}/settings/log-drains`
: '/project',
icon: Settings,
items: projectSettingsItems.map((item) => ({
...item,
isActive: router.pathname.includes(item.url),
})),
isActive: isProjectSettings,
},
]
: []
const databaseItems =
isProjectScope && ref
? generateDatabaseNavItems(ref, project, {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
showPgReplicate,
enablePgReplicate,
showRoles,
showWrappers,
pathname: router.pathname,
})
: []
const platformItems =
isProjectScope && ref
? generatePlatformNavItems(ref, project, {
authEnabled,
edgeFunctionsEnabled,
storageEnabled,
realtimeEnabled,
authOverviewPageEnabled,
pathname: router.pathname,
})
: []
const observabilityItems =
isProjectScope && ref
? generateObservabilityNavItems(ref, project, {
showReports,
unifiedLogs: isUnifiedLogsEnabled,
pathname: router.pathname,
})
: []
const { installedIntegrations } = useInstalledIntegrations()
const integrationsItems =
isProjectScope && ref
? [
{
title: 'Explore',
url: isProjectBuilding ? `/project/${ref}` : `/project/${ref}/integrations`,
icon: Compass,
isActive:
router.pathname === `/project/[ref]/integrations` ||
(router.pathname.includes('/integrations') &&
!installedIntegrations.some((int) =>
router.pathname.includes(`/integrations/${int.id}`)
)),
},
...installedIntegrations.map((integration) => ({
title: integration.name,
label: integration.status,
url: `/project/${ref}/integrations/${integration.id}/overview`,
icon: Blocks,
isActive: router.pathname.includes(`/integrations/${integration.id}`),
})),
]
: []
const activeOrganizationRoute = router.pathname.split('/')[3]
const organizationItems =
organizationSlug.length > 0
? [
{
title: 'Projects',
url: buildOrgHref(`/org/${organizationSlug}`),
icon: Boxes,
isActive: activeOrganizationRoute === undefined,
},
{
title: 'Team',
url: buildOrgHref(`/org/${organizationSlug}/team`),
icon: Users,
isActive: activeOrganizationRoute === 'team',
},
{
title: 'Integrations',
url: buildOrgHref(`/org/${organizationSlug}/integrations`),
icon: Blocks,
isActive: activeOrganizationRoute === 'integrations',
},
{
title: 'Usage',
url: buildOrgHref(`/org/${organizationSlug}/usage`),
icon: ChartArea,
isActive: activeOrganizationRoute === 'usage',
},
...(showBilling
? [
{
title: 'Billing',
url: buildOrgHref(`/org/${organizationSlug}/billing`),
icon: Receipt,
isActive: activeOrganizationRoute === 'billing',
},
]
: []),
{
title: 'Organization Settings',
url: buildOrgHref(`/org/${organizationSlug}/general`),
icon: Settings,
isActive:
router.pathname.includes('/general') ||
router.pathname.includes('/apps') ||
router.pathname.includes('/audit') ||
router.pathname.includes('/documents') ||
router.pathname.includes('/security') ||
router.pathname.includes('/sso'),
},
]
: []
isProjectScope,
isActiveHealthy,
projectItems,
databaseItems,
platformItems,
observabilityItems,
integrationsItems,
organizationItems,
} = useAppSidebarNavItems({ scope })
return (
<>
@@ -293,9 +51,7 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
<Button
type="outline"
size="small"
onClick={() => {
setCommandMenuOpen(true)
}}
onClick={() => setCommandMenuOpen(true)}
className="h-8 min-w-8 !w-8 px-0 justify-center text-foreground-lighter font-normal bg-transparent"
icon={<Search strokeWidth={1.5} />}
/>
@@ -303,9 +59,7 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
type="default"
size="small"
disabled={!isActiveHealthy}
onClick={() => {
setShowConnect(true)
}}
onClick={() => setShowConnect(true)}
className="h-8 flex-1 justify-center gap-0 pl-2"
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
>
@@ -41,16 +41,11 @@ export interface NavGroupProps {
}
export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupProps) {
// Load the sidebar state from localStorage using react-query for cross-component sync
const [sidebarState, setSidebarState] = useLocalStorageQuery<Record<string, boolean>>(
LOCAL_STORAGE_KEYS.DASHBOARD_SIDEBAR_STATE,
{}
)
// Get the current group's open state, defaulting to true (open)
const isOpen = id ? sidebarState[id] ?? true : true
// Handler to update the open state for this specific group
const handleOpenChange = (open: boolean) => {
if (id && isCollapsible) {
setSidebarState((prev) => ({ ...prev, [id]: open }))
@@ -17,10 +17,8 @@ import {
FileText,
Lightbulb,
Network,
ScrollText,
Settings,
Telescope,
Zap,
} from 'lucide-react'
import type { NavGroupItem } from './NavGroup'
@@ -62,13 +60,11 @@ export function generateDatabaseNavItems(
const buildingUrl = `/project/${ref}`
const {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
showPgReplicate,
enablePgReplicate,
showRoles,
showWrappers,
pathname = '',
} = flags || {}
@@ -0,0 +1,287 @@
import { useFlag, useIsMFAEnabled, useParams } from 'common'
import {
useIsColumnLevelPrivilegesEnabled,
useUnifiedLogsPreview,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Home } from 'icons'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import {
Blocks,
Boxes,
ChartArea,
Compass,
Receipt,
Settings,
Users,
} from 'lucide-react'
import { useRouter } from 'next/router'
import { useGenerateSettingsMenu } from '../ProjectSettingsLayout/SettingsMenu.utils'
import type { NavGroupItem } from './NavGroup'
import {
generateDatabaseNavItems,
generateObservabilityNavItems,
generatePlatformNavItems,
} from './NavigationV2.utils'
export type AppSidebarScope = 'project' | 'organization'
export interface UseAppSidebarNavItemsParams {
scope?: AppSidebarScope
}
export interface UseAppSidebarNavItemsResult {
ref: string
organizationSlug: string
isProjectScope: boolean
isActiveHealthy: boolean
isProjectBuilding: boolean
projectItems: NavGroupItem[]
databaseItems: NavGroupItem[]
platformItems: NavGroupItem[]
observabilityItems: NavGroupItem[]
integrationsItems: NavGroupItem[]
organizationItems: NavGroupItem[]
}
export function useAppSidebarNavItems(
params: UseAppSidebarNavItemsParams = {}
): UseAppSidebarNavItemsResult {
const router = useRouter()
const { ref: projectRef, slug } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const isUserMFAEnabled = useIsMFAEnabled()
const currentScope: AppSidebarScope =
params.scope ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
const isProjectScope = currentScope === 'project'
const ref = projectRef ?? project?.ref ?? ''
const organizationSlug = slug ?? (router.query.orgSlug as string | undefined) ?? ''
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
const settingsMenu = useGenerateSettingsMenu()
const { data: extensions } = useDatabaseExtensionsQuery(
{ projectRef: project?.ref, connectionString: project?.connectionString },
{ enabled: isProjectScope }
)
const { data: addons } = useProjectAddonsQuery(
{ projectRef: project?.ref },
{ enabled: isProjectScope }
)
const pgNetExtensionExists =
(extensions ?? []).find((ext) => ext.name === 'pg_net') !== undefined
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
const enablePgReplicate = useIsETLPrivateAlpha()
const {
databaseReplication: showPgReplicate,
databaseRoles: showRoles,
integrationsWrappers: showWrappers,
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
const {
projectAuthAll: authEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_edge_function:all',
'project_storage:all',
'realtime:all',
])
const authOverviewPageEnabled = useFlag('authOverviewPage')
const showReports = useIsFeatureEnabled('reports:all')
const showBilling = useIsFeatureEnabled('billing:all')
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
const disableAccessMfa =
selectedOrganization?.organization_requires_mfa === true && !isUserMFAEnabled
const buildOrgHref = (href: string) => (disableAccessMfa ? router.asPath : href)
const projectSettingsItems =
isProjectScope && ref
? settingsMenu
.flatMap((group) => {
if (group.title === 'Billing' && !selectedOrganization?.slug) return []
return group.items
})
.filter((item) => !item.disabled)
.map((item) => ({ title: item.name, url: item.url }))
: []
const isProjectHome =
router.pathname === `/project/[ref]` || router.pathname === `/project/${ref}`
const isProjectSettings = router.pathname.includes('/settings')
const projectItems: NavGroupItem[] = isProjectScope
? [
{
title: 'Home',
url: ref ? `/project/${ref}` : '/project',
icon: Home,
isActive: isProjectHome,
},
{
title: 'Project Settings',
url:
ref && IS_PLATFORM
? `/project/${ref}/settings/general`
: ref
? `/project/${ref}/settings/log-drains`
: '/project',
icon: Settings,
items: projectSettingsItems.map((item) => ({
...item,
isActive: router.pathname.includes(item.url),
})),
isActive: isProjectSettings,
},
]
: []
const databaseItems: NavGroupItem[] =
isProjectScope && ref
? generateDatabaseNavItems(ref, project, {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
showPgReplicate,
enablePgReplicate,
showRoles,
showWrappers,
pathname: router.pathname,
})
: []
const platformItems: NavGroupItem[] =
isProjectScope && ref
? generatePlatformNavItems(ref, project, {
authEnabled,
edgeFunctionsEnabled,
storageEnabled,
realtimeEnabled,
authOverviewPageEnabled,
pathname: router.pathname,
})
: []
const observabilityItems: NavGroupItem[] =
isProjectScope && ref
? generateObservabilityNavItems(ref, project, {
showReports,
unifiedLogs: isUnifiedLogsEnabled,
pathname: router.pathname,
})
: []
const { installedIntegrations } = useInstalledIntegrations()
const integrationsItems: NavGroupItem[] =
isProjectScope && ref
? [
{
title: 'Explore',
url: isProjectBuilding ? `/project/${ref}` : `/project/${ref}/integrations`,
icon: Compass,
isActive:
router.pathname === `/project/[ref]/integrations` ||
(router.pathname.includes('/integrations') &&
!installedIntegrations.some((int) =>
router.pathname.includes(`/integrations/${int.id}`)
)),
},
...installedIntegrations.map((integration) => ({
title: integration.name,
label: integration.status,
url: `/project/${ref}/integrations/${integration.id}/overview`,
icon: Blocks,
isActive: router.pathname.includes(`/integrations/${integration.id}`),
})),
]
: []
const activeOrganizationRoute = router.pathname.split('/')[3]
const organizationItems: NavGroupItem[] =
organizationSlug.length > 0
? [
{
title: 'Projects',
url: buildOrgHref(`/org/${organizationSlug}`),
icon: Boxes,
isActive: activeOrganizationRoute === undefined,
},
{
title: 'Team',
url: buildOrgHref(`/org/${organizationSlug}/team`),
icon: Users,
isActive: activeOrganizationRoute === 'team',
},
{
title: 'Integrations',
url: buildOrgHref(`/org/${organizationSlug}/integrations`),
icon: Blocks,
isActive: activeOrganizationRoute === 'integrations',
},
{
title: 'Usage',
url: buildOrgHref(`/org/${organizationSlug}/usage`),
icon: ChartArea,
isActive: activeOrganizationRoute === 'usage',
},
...(showBilling
? [
{
title: 'Billing',
url: buildOrgHref(`/org/${organizationSlug}/billing`),
icon: Receipt,
isActive: activeOrganizationRoute === 'billing',
},
]
: []),
{
title: 'Organization Settings',
url: buildOrgHref(`/org/${organizationSlug}/general`),
icon: Settings,
isActive:
router.pathname.includes('/general') ||
router.pathname.includes('/apps') ||
router.pathname.includes('/audit') ||
router.pathname.includes('/documents') ||
router.pathname.includes('/security') ||
router.pathname.includes('/sso'),
},
]
: []
return {
ref,
organizationSlug,
isProjectScope,
isActiveHealthy,
isProjectBuilding,
projectItems,
databaseItems,
platformItems,
observabilityItems,
integrationsItems,
organizationItems,
}
}
@@ -0,0 +1,149 @@
import { renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useAppSidebarNavItems } from 'components/layouts/NavigationV2/useAppSidebarNavItems'
// ─── Module mocks ─────────────────────────────────────────────────────────────
vi.mock('lib/constants', async () => {
const actual = await vi.importActual<Record<string, unknown>>('lib/constants')
return { ...actual, IS_PLATFORM: true }
})
vi.mock('next/router', () => ({
useRouter: vi.fn().mockReturnValue({
pathname: '/project/[ref]',
asPath: '/project/test-ref',
query: {},
}),
}))
vi.mock('common', () => ({
useFlag: vi.fn().mockReturnValue(false),
useParams: vi.fn().mockReturnValue({ ref: 'test-ref', slug: undefined }),
useIsMFAEnabled: vi.fn().mockReturnValue(false),
}))
vi.mock('hooks/misc/useSelectedOrganization', () => ({
useSelectedOrganizationQuery: vi
.fn()
.mockReturnValue({ data: { slug: 'my-org', organization_requires_mfa: false } }),
}))
vi.mock('hooks/misc/useSelectedProject', () => ({
useSelectedProjectQuery: vi
.fn()
.mockReturnValue({ data: { ref: 'test-ref', status: 'ACTIVE_HEALTHY' } }),
}))
vi.mock('hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: vi.fn((flags: string | string[]) => {
if (typeof flags === 'string') return true
return Object.fromEntries((flags as string[]).map((f) => [f.replace(/[:/]/g, ''), true]))
}),
}))
vi.mock('components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({
useIsColumnLevelPrivilegesEnabled: vi.fn().mockReturnValue(false),
useUnifiedLogsPreview: vi.fn().mockReturnValue({ isEnabled: false }),
}))
vi.mock('components/interfaces/Database/Replication/useIsETLPrivateAlpha', () => ({
useIsETLPrivateAlpha: vi.fn().mockReturnValue(false),
}))
vi.mock('components/interfaces/Integrations/Landing/useInstalledIntegrations', () => ({
useInstalledIntegrations: vi.fn().mockReturnValue({ installedIntegrations: [] }),
}))
vi.mock('data/database-extensions/database-extensions-query', () => ({
useDatabaseExtensionsQuery: vi.fn().mockReturnValue({ data: undefined }),
}))
vi.mock('data/subscriptions/project-addons-query', () => ({
useProjectAddonsQuery: vi.fn().mockReturnValue({ data: undefined }),
}))
vi.mock('components/layouts/ProjectSettingsLayout/SettingsMenu.utils', () => ({
useGenerateSettingsMenu: vi.fn().mockReturnValue([]),
}))
// ─── Tests ────────────────────────────────────────────────────────────────────
describe('useAppSidebarNavItems', () => {
describe('scope detection', () => {
it('detects project scope from pathname', () => {
const { result } = renderHook(() => useAppSidebarNavItems())
expect(result.current.isProjectScope).toBe(true)
})
it('respects explicit "organization" scope override', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
expect(result.current.isProjectScope).toBe(false)
})
it('respects explicit "project" scope override', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
expect(result.current.isProjectScope).toBe(true)
})
})
describe('projectItems', () => {
it('includes Home and Project Settings when in project scope', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
const titles = result.current.projectItems.map((i) => i.title)
expect(titles).toContain('Home')
expect(titles).toContain('Project Settings')
})
it('returns empty projectItems in organization scope', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
expect(result.current.projectItems).toHaveLength(0)
})
it('Home item is active when on the project home route', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
const home = result.current.projectItems.find((i) => i.title === 'Home')
expect(home?.isActive).toBe(true)
})
it('Home url points to the current project ref', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
const home = result.current.projectItems.find((i) => i.title === 'Home')
expect(home?.url).toBe('/project/test-ref')
})
it('Project Settings url uses general settings on IS_PLATFORM', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'project' }))
const settings = result.current.projectItems.find((i) => i.title === 'Project Settings')
expect(settings?.url).toBe('/project/test-ref/settings/general')
})
})
describe('organizationItems', () => {
it('returns empty when no organizationSlug is available', () => {
const { result } = renderHook(() => useAppSidebarNavItems({ scope: 'organization' }))
// useParams returns slug: undefined, so organizationSlug = ''
expect(result.current.organizationItems).toHaveLength(0)
})
})
describe('isActiveHealthy', () => {
it('returns true when project status is ACTIVE_HEALTHY', () => {
const { result } = renderHook(() => useAppSidebarNavItems())
expect(result.current.isActiveHealthy).toBe(true)
})
})
describe('ref and organizationSlug', () => {
it('exposes the current project ref', () => {
const { result } = renderHook(() => useAppSidebarNavItems())
expect(result.current.ref).toBe('test-ref')
})
it('exposes an empty organizationSlug when none is set', () => {
const { result } = renderHook(() => useAppSidebarNavItems())
expect(result.current.organizationSlug).toBe('')
})
})
})