-
Notifications
You must be signed in to change notification settings - Fork 13.9k
feat(sidebar): add SidebarRail collapsed icon navigation rail #40558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0818444
723450d
96b6ceb
f37fd7a
e612864
2eb1614
b429ee5
3e5b327
982a633
843f917
982e88d
c2ded85
c409982
77e852b
1e64755
70fd502
000a5e9
3b10fa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { Divider } from '@rocket.chat/fuselage'; | ||
| import type { ComponentProps } from 'react'; | ||
|
|
||
| type HorizontalDividerProps = Omit<ComponentProps<typeof Divider>, 'vertical'>; | ||
|
|
||
| const HorizontalDivider = (props: HorizontalDividerProps) => <Divider {...props} vertical={false} />; | ||
|
|
||
| export default HorizontalDivider; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './HorizontalDivider'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { Box } from '@rocket.chat/fuselage'; | ||
| import { mockAppRoot } from '@rocket.chat/mock-providers'; | ||
| import { SessionContext } from '@rocket.chat/ui-contexts'; | ||
| import type { SessionContextValue } from '@rocket.chat/ui-contexts'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| import SidebarRail from './SidebarRail'; | ||
|
|
||
| const sessionMock = (state: Record<string, unknown>): SessionContextValue => ({ | ||
| query: (name) => [() => () => undefined, () => state[name]], | ||
| dispatch: () => undefined, | ||
| }); | ||
|
|
||
| const baseRoot = () => | ||
| mockAppRoot().withSetting('Layout_Show_Home_Button', true).withTranslations('en', 'core', { | ||
| Sidebar: 'Sidebar', | ||
| Home: 'Home', | ||
| Create_new: 'Create new', | ||
| Voice_Call: 'Voice Call', | ||
| Pages_and_actions: 'Pages and actions', | ||
| Workspace_and_user_preferences: 'Workspace and user preferences', | ||
| }); | ||
|
|
||
| export default { | ||
| title: 'Sidebar/SidebarRail', | ||
|
|
||
| component: SidebarRail, | ||
| parameters: { | ||
| layout: 'fullscreen', | ||
| }, | ||
| decorators: [ | ||
| (Story) => ( | ||
| <Box height='100vh' display='flex'> | ||
| <Story /> | ||
| </Box> | ||
| ), | ||
| ], | ||
| } satisfies Meta<typeof SidebarRail>; | ||
|
|
||
| type Story = StoryObj<typeof SidebarRail>; | ||
|
|
||
| export const Anonymous: Story = { | ||
| decorators: [baseRoot().buildStoryDecorator()], | ||
| }; | ||
|
|
||
| export const LoggedIn: Story = { | ||
| decorators: [baseRoot().withJohnDoe().buildStoryDecorator()], | ||
| }; | ||
|
|
||
| export const WithUnreadBadge: Story = { | ||
| decorators: [ | ||
| baseRoot() | ||
| .withJohnDoe() | ||
| .wrap((children: ReactNode) => <SessionContext.Provider value={sessionMock({ unread: 5 })}>{children}</SessionContext.Provider>) | ||
| .buildStoryDecorator(), | ||
| ], | ||
| }; | ||
|
|
||
| export const WithCreatePermissions: Story = { | ||
| decorators: [ | ||
| baseRoot().withJohnDoe().withPermission('create-c').withPermission('create-p').withPermission('create-d').buildStoryDecorator(), | ||
| ], | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { Box, NavBarGroup } from '@rocket.chat/fuselage'; | ||
| import { useUser } from '@rocket.chat/ui-contexts'; | ||
| import { memo } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import SidebarRailCreateNew from './SidebarRailCreateNew'; | ||
| import SidebarRailDivider from './SidebarRailDivider'; | ||
| import SidebarRailLoginPage from './SidebarRailLoginPage'; | ||
| import SidebarRailPhone from './SidebarRailPhone'; | ||
| import SidebarRailSort from './SidebarRailSort'; | ||
| import NavBarItemDirectoryPage from '../../navbar/NavBarPagesGroup/NavBarItemDirectoryPage'; | ||
| import NavBarItemHomePage from '../../navbar/NavBarPagesGroup/NavBarItemHomePage'; | ||
| import NavBarItemMarketPlaceMenu from '../../navbar/NavBarPagesGroup/NavBarItemMarketPlaceMenu'; | ||
| import { NavBarItemAdministrationMenu, UserMenu } from '../../navbar/NavBarSettingsToolbar'; | ||
|
|
||
| const SidebarRail = () => { | ||
| const { t } = useTranslation(); | ||
| const user = useUser(); | ||
|
|
||
| return ( | ||
| <Box | ||
| is='nav' | ||
| aria-label={t('Sidebar')} | ||
| className='rcx-sidebar-rail' | ||
| bg='surface-sidebar' | ||
| borderInlineEndWidth='default' | ||
| borderInlineEndStyle='solid' | ||
| borderInlineEndColor='stroke-light' | ||
| display='flex' | ||
| flexDirection='column' | ||
| alignItems='stretch' | ||
| width='x44' | ||
| height='full' | ||
| > | ||
| <Box flexGrow={1} minHeight={0} overflow='hidden auto' p={8}> | ||
| <NavBarGroup vertical aria-label={t('Pages_and_actions')}> | ||
| <NavBarItemHomePage title={t('Home')} /> | ||
| <SidebarRailSort /> | ||
| <SidebarRailCreateNew /> | ||
| </NavBarGroup> | ||
| <SidebarRailDivider /> | ||
| <NavBarGroup vertical aria-label={t('Voice_Call')}> | ||
| <SidebarRailPhone /> | ||
| <NavBarItemDirectoryPage title={t('Directory')} /> | ||
| <NavBarItemMarketPlaceMenu /> | ||
| </NavBarGroup> | ||
| </Box> | ||
| <Box p={8}> | ||
| <NavBarGroup vertical aria-label={t('Workspace_and_user_preferences')}> | ||
| <NavBarItemAdministrationMenu /> | ||
| {user ? <UserMenu user={user} /> : <SidebarRailLoginPage />} | ||
| </NavBarGroup> | ||
| </Box> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default memo(SidebarRail); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { Box, Sidepanel } from '@rocket.chat/fuselage'; | ||
| import { MediaCallWidgetSlot, useWidgetExternalControls } from '@rocket.chat/ui-voip'; | ||
| import { useLayoutEffect } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| const SidebarRailCallPanel = () => { | ||
| const { t } = useTranslation(); | ||
| const { openDialer, closeDialer } = useWidgetExternalControls(); | ||
|
|
||
| // The call panel hosts the dialer: while it is mounted and the session is idle | ||
| // (initial open, or right after a call ends) show the dialer instead of an empty | ||
| // panel. `openDialer` is idempotent (only acts on the "closed" state), so React | ||
| // StrictMode double-invoking this layout effect on mount is harmless. | ||
| useLayoutEffect(() => { | ||
| openDialer(); | ||
| }, [openDialer]); | ||
|
|
||
| // Leaving the telephony screen with only the idle dialer open must drop it, so it | ||
| // does not pop out as a floating widget. The `state === 'new'` guard scopes this to | ||
| // the idle dialer (never an ongoing call) and skips StrictMode's fake unmount, where | ||
| // the just-issued open has not re-rendered yet. | ||
| useLayoutEffect( | ||
| () => () => { | ||
| closeDialer(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The cleanup effect now calls Prompt for AI agents |
||
| }, | ||
| [closeDialer], | ||
| ); | ||
|
|
||
| return ( | ||
| <Box width='x280' minWidth='x280'> | ||
| <Sidepanel role='complementary' aria-label={t('Calls')}> | ||
| <Box p={16}> | ||
| <MediaCallWidgetSlot /> | ||
| </Box> | ||
| </Sidepanel> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default SidebarRailCallPanel; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { NavBarItem } from '@rocket.chat/fuselage'; | ||
| import { GenericMenu } from '@rocket.chat/ui-client'; | ||
| import type { HTMLAttributes } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useCreateNewMenu } from '../../navbar/NavBarPagesGroup/hooks/useCreateNewMenu'; | ||
|
|
||
| type SidebarRailCreateNewProps = Omit<HTMLAttributes<HTMLElement>, 'is'>; | ||
|
|
||
| const SidebarRailCreateNew = (props: SidebarRailCreateNewProps) => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| const sections = useCreateNewMenu(); | ||
|
|
||
| if (sections.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return <GenericMenu icon='pencil-box' sections={sections} title={t('Create_new')} is={NavBarItem} placement='right-start' {...props} />; | ||
| }; | ||
|
|
||
| export default SidebarRailCreateNew; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { ComponentProps } from 'react'; | ||
|
|
||
| import HorizontalDivider from '../../components/HorizontalDivider'; | ||
|
|
||
| type SidebarRailDividerProps = ComponentProps<typeof HorizontalDivider>; | ||
|
|
||
| const SidebarRailDivider = (props: SidebarRailDividerProps) => ( | ||
| <HorizontalDivider mbs={16} mbe={16} mi={4} borderBlockStartColor='stroke-light' {...props} /> | ||
| ); | ||
|
|
||
| export default SidebarRailDivider; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { Box, NavBar as NavBarComponent, NavBarSection } from '@rocket.chat/fuselage'; | ||
|
|
||
| import NavBarNavigation from '../../navbar/NavBarNavigation'; | ||
|
|
||
| const SidebarRailHeader = () => ( | ||
| <NavBarComponent aria-label='header' style={{ paddingInline: '0.5rem' }}> | ||
| <NavBarSection> | ||
| <Box is='img' src='/images/logo/icon.svg' alt='Rocket.Chat' size='x28' /> | ||
| </NavBarSection> | ||
| <NavBarNavigation /> | ||
| <NavBarSection /> | ||
| </NavBarComponent> | ||
| ); | ||
|
|
||
| export default SidebarRailHeader; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { NavBarItem } from '@rocket.chat/fuselage'; | ||
| import { useSessionDispatch } from '@rocket.chat/ui-contexts'; | ||
| import type { HTMLAttributes } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| type SidebarRailLoginPageProps = Omit<HTMLAttributes<HTMLElement>, 'is'>; | ||
|
|
||
| const SidebarRailLoginPage = (props: SidebarRailLoginPageProps) => { | ||
| const setForceLogin = useSessionDispatch('forceLogin'); | ||
| const { t } = useTranslation(); | ||
|
|
||
| return <NavBarItem {...props} icon='login' title={t('Login')} onClick={() => setForceLogin(true)} />; | ||
| }; | ||
|
|
||
| export default SidebarRailLoginPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { NavBarItem } from '@rocket.chat/fuselage'; | ||
| import { useStableCallback } from '@rocket.chat/fuselage-hooks'; | ||
| import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; | ||
| import { useMediaCallAction } from '@rocket.chat/ui-voip'; | ||
| import type { HTMLAttributes } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| type SidebarRailPhoneProps = Omit<HTMLAttributes<HTMLElement>, 'is'>; | ||
|
|
||
| const SidebarRailPhone = (props: SidebarRailPhoneProps) => { | ||
| const { t } = useTranslation(); | ||
| const callAction = useMediaCallAction(); | ||
| const router = useRouter(); | ||
| const currentRoute = useCurrentRoutePath(); | ||
|
|
||
| const isActive = currentRoute?.includes('/call-history') ?? false; | ||
|
|
||
| const handleClick = useStableCallback(() => { | ||
| router.navigate('/call-history'); | ||
| }); | ||
|
|
||
| if (!callAction) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <NavBarItem | ||
| {...props} | ||
| title={t('Calls')} | ||
| icon='phone' | ||
| pressed={isActive} | ||
| aria-current={isActive ? 'page' : undefined} | ||
| onClick={handleClick} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default SidebarRailPhone; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { NavBarItem } from '@rocket.chat/fuselage'; | ||
| import { GenericMenu } from '@rocket.chat/ui-client'; | ||
| import type { HTMLAttributes } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useSortMenu } from '../../navbar/NavBarPagesGroup/hooks/useSortMenu'; | ||
|
|
||
| type SidebarRailSortProps = Omit<HTMLAttributes<HTMLElement>, 'is'>; | ||
|
|
||
| const SidebarRailSort = (props: SidebarRailSortProps) => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| const sections = useSortMenu(); | ||
|
|
||
| return ( | ||
| <GenericMenu | ||
| icon='sort' | ||
| sections={sections} | ||
| title={t('Display')} | ||
| selectionMode='multiple' | ||
| is={NavBarItem} | ||
| placement='right-start' | ||
| {...props} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default SidebarRailSort; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './SidebarRail'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
DDPCommon.parseDDP/stringifyDDPshould preserve DDP payload shape; the current no-op stub can break any Storybook path that decodes or re-encodes messages.Prompt for AI agents