Skip to content

Commit

Permalink
add theme and language selections in user menu (#593)
Browse files Browse the repository at this point in the history
* add theme selection in user menu

* add language selector in user nav

* fix type issues in design system
  • Loading branch information
abhinavrastogi-harness authored Dec 19, 2024
1 parent 0e2fbf6 commit 71a1f2f
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 91 deletions.
15 changes: 13 additions & 2 deletions apps/design-system/src/pages/view-preview/root-view-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom'
import { NavbarItemType, NavState } from '@harnessio/ui/components'
import { SandboxRoot } from '@harnessio/ui/views'

import { mockT, noop } from '../../utils.ts'
import { noop, useThemeStore, useTranslationsStore } from '../../utils.ts'

const RootViewWrapper: FC<PropsWithChildren<{ asChild?: boolean }>> = ({ children, asChild = false }) => {
const [pinnedMenu, setPinnedMenu] = useState<NavbarItemType[]>([])
Expand Down Expand Up @@ -36,7 +36,18 @@ const RootViewWrapper: FC<PropsWithChildren<{ asChild?: boolean }>> = ({ childre

return (
<Routes>
<Route path="*" element={<SandboxRoot useNav={useNav} currentUser={undefined} logout={noop} t={mockT as any} />}>
<Route
path="*"
element={
<SandboxRoot
useNav={useNav}
currentUser={undefined}
logout={noop}
useThemeStore={useThemeStore}
useTranslationStore={useTranslationsStore}
/>
}
>
{asChild ? children : <Route path="*" element={children} />}
</Route>
</Routes>
Expand Down
1 change: 1 addition & 0 deletions apps/design-system/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const mockT = (...args: unknown[]) => {
}

export const useTranslationsStore = () => ({ t: mockT as any, changeLanguage: noop, i18n: {} as any })
export const useThemeStore = () => ({ theme: 'dark-std-std' as any, setTheme: noop })
14 changes: 11 additions & 3 deletions apps/gitness/src/components/RootWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'

import { SandboxRoot } from '@harnessio/ui/views'

import { useAppContext } from '../framework/context/AppContext'
import { useThemeStore } from '../framework/context/ThemeContext'
import { useTranslationStore } from '../i18n/stores/i18n-store'
import { useNav } from './stores/recent-pinned-nav-links.store'

const RootWrapper = () => {
const { currentUser } = useAppContext()
const navigate = useNavigate()
const { t } = useTranslation()

return <SandboxRoot t={t} currentUser={currentUser} useNav={useNav} logout={() => navigate('/logout')} />
return (
<SandboxRoot
currentUser={currentUser}
useNav={useNav}
useThemeStore={useThemeStore}
useTranslationStore={useTranslationStore}
logout={() => navigate('/logout')}
/>
)
}

export default RootWrapper
2 changes: 2 additions & 0 deletions apps/gitness/src/i18n/stores/i18n-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ export const useTranslationStore = create<TranslationStore>(_set => ({
localStorage.setItem('i18nextLng', lng)
localStorage.removeItem('i18nextLngSystem')
}
// temporary solution to reload the page to apply the new language
window.location.reload()
}
}))
163 changes: 91 additions & 72 deletions packages/ui/src/components/navbar/navbar-user/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Fragment, useMemo } from 'react'
import { Link } from 'react-router-dom'

import {
Expand All @@ -8,18 +7,23 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
FullTheme,
Icon,
IThemeStore,
Text
} from '@/components'
import { TypesUser } from '@/types'
import { cn } from '@utils/cn'
import { getInitials } from '@utils/stringUtils'
import { TFunction } from 'i18next'

import { getUserMenuItems } from '../data'
import { UserMenuKeys } from '../types'
import { TranslationStore } from '@views/repo'

interface UserBlockProps {
username: string
Expand Down Expand Up @@ -60,54 +64,20 @@ interface NavbarUserProps {
currentUser: TypesUser | undefined
handleCustomNav: () => void
handleLogOut: () => void
t: TFunction
useThemeStore: () => IThemeStore
useTranslationStore: () => TranslationStore
}

export const NavbarUser = ({ currentUser, handleCustomNav, handleLogOut, t }: NavbarUserProps) => {
export const NavbarUser = ({
currentUser,
handleCustomNav,
handleLogOut,
useThemeStore,
useTranslationStore
}: NavbarUserProps) => {
const username = currentUser?.display_name || currentUser?.uid || ''
const userMenuItems = getUserMenuItems(t)

const menuItems = useMemo(() => {
return userMenuItems.map(({ key, iconName, title, to, isSeparated }) => {
const className = 'relative grid grid-cols-[auto_1fr] items-center gap-2.5'

const handleClick = () => {
switch (key) {
case UserMenuKeys.CUSTOM_NAV:
return handleCustomNav()
case UserMenuKeys.LOG_OUT:
return handleLogOut()
default:
return
}
}

const elementChild = (
<>
<Icon className={cn('text-icons-4 ml-[3px] transition-colors')} size={12} name={iconName} />
<Text size={2} truncate>
{title}
</Text>
</>
)

const element = to ? (
<Link className={className} to={to}>
{elementChild}
</Link>
) : (
<button className={cn(className, 'w-full text-left')} onClick={handleClick}>
{elementChild}
</button>
)

return {
key,
element,
isSeparated
}
})
}, [handleCustomNav, handleLogOut, userMenuItems])
const { theme, setTheme } = useThemeStore()
const { t, i18n, changeLanguage } = useTranslationStore()

return (
<DropdownMenu>
Expand All @@ -116,28 +86,77 @@ export const NavbarUser = ({ currentUser, handleCustomNav, handleLogOut, t }: Na
<UserBlock username={username} email={currentUser?.email} url={currentUser?.url} isButton />
</div>
</DropdownMenuTrigger>

{menuItems && (
<DropdownMenuContent
className="ml-3 w-[230px] !rounded-lg bg-background-1"
align="start"
sideOffset={-40}
alignOffset={187}
>
<UserBlock className="p-2" username={username} email={currentUser?.email} url={currentUser?.url} />
<DropdownMenuSeparator />
{menuItems.map(itm => {
return (
<Fragment key={itm.key}>
{!!itm?.isSeparated && <DropdownMenuSeparator />}
<DropdownMenuItem className="[&_svg]:data-[highlighted]:text-icons-2" asChild>
{itm.element}
</DropdownMenuItem>
</Fragment>
)
})}
</DropdownMenuContent>
)}
<DropdownMenuContent
className="ml-3 w-[230px] !rounded-lg bg-background-1"
align="start"
sideOffset={-40}
alignOffset={187}
>
<UserBlock className="p-2" username={username} email={currentUser?.email} url={currentUser?.url} />
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/account">
<Icon size={12} name="user" className="mr-2" />
<Text>{t('component:navbar.account', 'Account')}</Text>
</Link>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icon size={12} name="paint" className="mr-2" />
<Text>{t('component:navbar.theme', 'Theme')}</Text>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={theme}
onValueChange={value => {
setTheme(value as FullTheme)
}}
>
<DropdownMenuRadioItem value="light-std-std">Light</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark-std-std">Dark</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system-std-std">System</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icon size={12} name="environment" className="mr-2" />
<Text>Language</Text>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={i18n.language}
onValueChange={value => {
changeLanguage(value)
}}
>
<DropdownMenuRadioItem value="en">English</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="fr">French</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">System</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem onClick={handleCustomNav}>
<Icon size={12} name="navigation" className="mr-2" />
<Text>{t('component:navbar.customNav', 'Customize navigation')}</Text>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/account">
<Icon size={12} name="settings-1" className="mr-2" />
<Text>{t('component:navbar.administration', 'Administration')}</Text>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogOut}>
<Icon size={12} name="logOut" className="mr-2" />
<Text>{t('component:navbar.logout', 'Log out')}</Text>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
19 changes: 14 additions & 5 deletions packages/ui/src/components/navbar/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useMemo } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'

import { Button, Icon, NavbarProjectChooser, ScrollArea, Spacer } from '@/components'
import { Button, Icon, IThemeStore, NavbarProjectChooser, ScrollArea, Spacer } from '@/components'
import { TypesUser } from '@/types'
import { TFunction } from 'i18next'
import { TranslationStore } from '@views/index'
import { isEmpty } from 'lodash-es'

import { getAdminMenuItem } from './data'
Expand All @@ -26,7 +26,8 @@ interface NavbarProps {
handleLogOut: () => void
handleChangePinnedMenuItem: (item: NavbarItemType, pin: boolean) => void
handleRemoveRecentMenuItem: (item: NavbarItemType) => void
t: TFunction
useThemeStore: () => IThemeStore
useTranslationStore: () => TranslationStore
}

export const Navbar = ({
Expand All @@ -41,10 +42,12 @@ export const Navbar = ({
handleLogOut,
handleChangePinnedMenuItem,
handleRemoveRecentMenuItem,
t
useThemeStore,
useTranslationStore
}: NavbarProps) => {
const location = useLocation()
const navigate = useNavigate()
const { t } = useTranslationStore()
const adminMenuItem = getAdminMenuItem(t)
const showNavbar = useMemo(() => {
return !hideNavbarPaths.includes(location.pathname)
Expand Down Expand Up @@ -116,7 +119,13 @@ export const Navbar = ({

<NavbarSkeleton.Footer>
{!isEmpty(currentUser) ? (
<NavbarUser currentUser={currentUser} handleCustomNav={handleCustomNav} handleLogOut={handleLogOut} t={t} />
<NavbarUser
currentUser={currentUser}
handleCustomNav={handleCustomNav}
handleLogOut={handleLogOut}
useTranslationStore={useTranslationStore}
useThemeStore={useThemeStore}
/>
) : (
<Button onClick={() => navigate('/signin')}>Sign In</Button>
)}
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/icons/account.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions packages/ui/src/views/layouts/SandboxRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Outlet, useLocation } from 'react-router-dom'

import { ManageNavigation, MoreSubmenu, Navbar, SettingsMenu } from '@/components'
import { IThemeStore, ManageNavigation, MoreSubmenu, Navbar, SettingsMenu } from '@/components'
import { getNavbarMenuData } from '@/data/navbar-menu-data'
import { getPinnedMenuItemsData } from '@/data/pinned-menu-items-data'
import type { TypesUser } from '@/types'
import { MenuGroupType, MenuGroupTypes, NavbarItemType, NavState } from '@components/navbar/types'
import { useLocationChange } from '@hooks/use-location-change'
import { TFunction } from 'i18next'

import { SandboxLayout } from '../index'
import { SandboxLayout, TranslationStore } from '../index'

interface NavLinkStorageInterface {
state: {
Expand All @@ -22,13 +21,14 @@ export interface SandboxRootProps {
currentUser: TypesUser | undefined
logout?: () => void
useNav: () => NavState
t: TFunction
useThemeStore: () => IThemeStore
useTranslationStore: () => TranslationStore
}

export const SandboxRoot = ({ currentUser, useNav, t, logout }: SandboxRootProps) => {
export const SandboxRoot = ({ currentUser, useNav, logout, useThemeStore, useTranslationStore }: SandboxRootProps) => {
const location = useLocation()
const { pinnedMenu, recentMenu, setPinned, setRecent, setNavLinks } = useNav()

const { t } = useTranslationStore()
const [showMoreMenu, setShowMoreMenu] = useState(false)
const [showSettingMenu, setShowSettingMenu] = useState(false)
const [showCustomNav, setShowCustomNav] = useState(false)
Expand Down Expand Up @@ -168,7 +168,8 @@ export const SandboxRoot = ({ currentUser, useNav, t, logout }: SandboxRootProps
pinnedMenuItems={pinnedMenu}
handleChangePinnedMenuItem={handleChangePinnedMenuItem}
handleRemoveRecentMenuItem={handleRemoveRecentMenuItem}
t={t}
useThemeStore={useThemeStore}
useTranslationStore={useTranslationStore}
/>
</SandboxLayout.LeftPanel>
<div className="flex flex-col">
Expand Down

0 comments on commit 71a1f2f

Please sign in to comment.