feat: improve mobile display by tab bar and other changes (#2304)

* feat: improve mobile display by tab bar and other changes

* fix: remove unused declaration in mobile navigation

* fix: update desktop navbar stories title

* fix: retrieve old titles for desktop-navbar stories

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: update logic for tab bar menu icons

* fix: remove Settings icon for mobile

* fix: resolve comments in pl

* feat: rework mobile navigation bar

* Fix

* Fixes

---------

Co-authored-by: Thaïs Guigon <guigon.thais@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Saba Shavidze
2023-12-02 02:16:34 +04:00
committed by GitHub
parent 74b077f3ca
commit fec8223ab8
50 changed files with 640 additions and 380 deletions

View File

@ -1,72 +0,0 @@
import { useLocation } from 'react-router-dom';
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Favorites } from '@/favorites/components/Favorites';
import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems';
import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
import {
IconBell,
IconCheckbox,
IconSearch,
IconSettings,
IconTargetArrow,
} from '@/ui/display/icon/index';
import { useIsSubMenuNavbarDisplayed } from '@/ui/layout/hooks/useIsSubMenuNavbarDisplayed';
import MainNavbar from '@/ui/navigation/navbar/components/MainNavbar';
import NavItem from '@/ui/navigation/navbar/components/NavItem';
import NavTitle from '@/ui/navigation/navbar/components/NavTitle';
export const AppNavbar = () => {
const currentPath = useLocation().pathname;
const { toggleCommandMenu } = useCommandMenu();
const isInSubMenu = useIsSubMenuNavbarDisplayed();
const { currentUserDueTaskCount } = useCurrentUserTaskCount();
return (
<>
{!isInSubMenu ? (
<MainNavbar>
<NavItem
label="Search"
Icon={IconSearch}
onClick={() => {
toggleCommandMenu();
}}
keyboard={['⌘', 'K']}
/>
<NavItem
label="Notifications"
to="/inbox"
Icon={IconBell}
soon={true}
/>
<NavItem
label="Settings"
to="/settings/profile"
Icon={IconSettings}
/>
<NavItem
label="Tasks"
to="/tasks"
active={currentPath === '/tasks'}
Icon={IconCheckbox}
count={currentUserDueTaskCount}
/>
<Favorites />
<NavTitle label="Workspace" />
<ObjectMetadataNavItems />
<NavItem
label="Opportunities"
to="/objects/opportunities"
active={currentPath === '/objects/opportunities'}
Icon={IconTargetArrow}
/>
</MainNavbar>
) : (
<SettingsNavbar />
)}
</>
);
};

View File

@ -2,8 +2,8 @@ import styled from '@emotion/styled';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import NavItem from '@/ui/navigation/navbar/components/NavItem'; import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { useFavorites } from '../hooks/useFavorites'; import { useFavorites } from '../hooks/useFavorites';

View File

@ -0,0 +1,64 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Favorites } from '@/favorites/components/Favorites';
import { useIsTasksPage } from '@/navigation/hooks/useIsTasksPage';
import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
import {
IconBell,
IconCheckbox,
IconSearch,
IconSettings,
} from '@/ui/display/icon/index';
import MainNavbar from '@/ui/navigation/navigation-drawer/components/MainNavbar';
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
import { WorkspaceNavItems } from './WorkspaceNavItems';
export const DesktopNavigationDrawer = () => {
const { toggleCommandMenu } = useCommandMenu();
const isSettingsPage = useIsSettingsPage();
const isTasksPage = useIsTasksPage();
const { currentUserDueTaskCount } = useCurrentUserTaskCount();
const navigate = useNavigate();
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
return isSettingsPage ? (
<SettingsNavbar />
) : (
<MainNavbar>
<NavItem
label="Search"
Icon={IconSearch}
onClick={toggleCommandMenu}
keyboard={['⌘', 'K']}
/>
<NavItem label="Notifications" to="/inbox" Icon={IconBell} soon />
<NavItem
label="Settings"
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
navigate('/settings/profile');
}}
Icon={IconSettings}
/>
<NavItem
label="Tasks"
to="/tasks"
active={isTasksPage}
Icon={IconCheckbox}
count={currentUserDueTaskCount}
/>
<Favorites />
<WorkspaceNavItems />
</MainNavbar>
);
};

View File

@ -0,0 +1,85 @@
import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import {
IconCheckbox,
IconList,
IconSearch,
IconSettings,
} from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
import { useIsTasksPage } from '../hooks/useIsTasksPage';
type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
export const MobileNavigationBar = () => {
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
const { closeCommandMenu, toggleCommandMenu } = useCommandMenu();
const isTasksPage = useIsTasksPage();
const isSettingsPage = useIsSettingsPage();
const navigate = useNavigate();
const [navigationDrawer, setNavigationDrawer] = useRecoilState(
navigationDrawerState,
);
const initialActiveItemName: NavigationBarItemName = isCommandMenuOpened
? 'search'
: isTasksPage
? 'tasks'
: isSettingsPage
? 'settings'
: 'main';
const items: {
name: NavigationBarItemName;
Icon: IconComponent;
onClick: () => void;
}[] = [
{
name: 'main',
Icon: IconList,
onClick: () => {
closeCommandMenu();
setNavigationDrawer(navigationDrawer === 'main' ? '' : 'main');
},
},
{
name: 'search',
Icon: IconSearch,
onClick: () => {
setNavigationDrawer('');
toggleCommandMenu();
},
},
{
name: 'tasks',
Icon: IconCheckbox,
onClick: () => {
closeCommandMenu();
setNavigationDrawer('');
navigate('/tasks');
},
},
{
name: 'settings',
Icon: IconSettings,
onClick: () => {
closeCommandMenu();
setNavigationDrawer(navigationDrawer === 'settings' ? '' : 'settings');
},
},
];
return (
<NavigationBar
activeItemName={navigationDrawer || initialActiveItemName}
items={items}
/>
);
};

View File

@ -0,0 +1,21 @@
import { useRecoilState } from 'recoil';
import { Favorites } from '@/favorites/components/Favorites';
import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
import MainNavbar from '@/ui/navigation/navigation-drawer/components/MainNavbar';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { WorkspaceNavItems } from './WorkspaceNavItems';
export const MobileNavigationDrawer = () => {
const [navigationDrawer] = useRecoilState(navigationDrawerState);
return navigationDrawer === 'settings' ? (
<SettingsNavbar />
) : (
<MainNavbar>
<Favorites />
<WorkspaceNavItems />
</MainNavbar>
);
};

View File

@ -0,0 +1,23 @@
import { useLocation } from 'react-router-dom';
import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems';
import { IconTargetArrow } from '@/ui/display/icon/index';
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
export const WorkspaceNavItems = () => {
const { pathname } = useLocation();
return (
<>
<NavTitle label="Workspace" />
<ObjectMetadataNavItems />
<NavItem
label="Opportunities"
to="/objects/opportunities"
active={pathname === '/objects/opportunities'}
Icon={IconTargetArrow}
/>
</>
);
};

View File

@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { DesktopNavigationDrawer } from '../DesktopNavigationDrawer';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const meta: Meta<typeof DesktopNavigationDrawer> = {
title: 'Modules/Navigation/DesktopNavigationDrawer',
component: DesktopNavigationDrawer,
};
export default meta;
type Story = StoryObj<typeof DesktopNavigationDrawer>;
export const Default: Story = {
decorators: [
ComponentDecorator,
ComponentWithRouterDecorator,
SnackBarDecorator,
],
};

View File

@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { MobileNavigationDrawer } from '../MobileNavigationDrawer';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const meta: Meta<typeof MobileNavigationDrawer> = {
title: 'Modules/Navigation/MobileNavigationDrawer',
component: MobileNavigationDrawer,
};
export default meta;
type Story = StoryObj<typeof MobileNavigationDrawer>;
export const Default: Story = {
decorators: [
ComponentDecorator,
ComponentWithRouterDecorator,
SnackBarDecorator,
],
};

View File

@ -0,0 +1,4 @@
import { useLocation } from 'react-router-dom';
export const useIsSettingsPage = () =>
useLocation().pathname.match(/\/settings\//g) !== null;

View File

@ -0,0 +1,3 @@
import { useLocation } from 'react-router-dom';
export const useIsTasksPage = () => useLocation().pathname === '/tasks';

View File

@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { Icon123 } from '@/ui/input/constants/icons'; import { Icon123 } from '@/ui/input/constants/icons';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons'; import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import NavItem from '@/ui/navigation/navbar/components/NavItem'; import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
export const ObjectMetadataNavItems = () => { export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings(); const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();

View File

@ -89,12 +89,14 @@ export const RecordTableContainer = ({
}} }}
/> />
<RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} /> <RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} />
<RecordTable {
recordTableId={recordTableId} <RecordTable
viewBarId={viewBarId} recordTableId={recordTableId}
updateRecordMutation={updateEntity} viewBarId={viewBarId}
createRecord={createRecord} updateRecordMutation={updateEntity}
/> createRecord={createRecord}
/>
}
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -12,9 +12,9 @@ import {
IconUserCircle, IconUserCircle,
IconUsers, IconUsers,
} from '@/ui/display/icon/index'; } from '@/ui/display/icon/index';
import NavItem from '@/ui/navigation/navbar/components/NavItem'; import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
import SubMenuNavbar from '@/ui/navigation/navbar/components/SubMenuNavbar'; import SubMenuNavbar from '@/ui/navigation/navigation-drawer/components/SubMenuNavbar';
export const SettingsNavbar = () => { export const SettingsNavbar = () => {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -1,4 +1,4 @@
import { IconArchiveOff, IconDotsVertical, IconTrash } from '@/ui/display/icon'; import { IconArchiveOff, IconDotsVertical } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
@ -15,9 +15,7 @@ type SettingsObjectFieldDisabledActionDropdownProps = {
}; };
export const SettingsObjectFieldDisabledActionDropdown = ({ export const SettingsObjectFieldDisabledActionDropdown = ({
isCustomField,
onActivate, onActivate,
onErase,
scopeKey, scopeKey,
}: SettingsObjectFieldDisabledActionDropdownProps) => { }: SettingsObjectFieldDisabledActionDropdownProps) => {
const dropdownScopeId = `${scopeKey}-settings-field-disabled-action-dropdown`; const dropdownScopeId = `${scopeKey}-settings-field-disabled-action-dropdown`;
@ -29,10 +27,10 @@ export const SettingsObjectFieldDisabledActionDropdown = ({
closeDropdown(); closeDropdown();
}; };
const handleErase = () => { // const handleErase = () => {
onErase(); // onErase();
closeDropdown(); // closeDropdown();
}; // };
return ( return (
<DropdownScope dropdownScopeId={dropdownScopeId}> <DropdownScope dropdownScopeId={dropdownScopeId}>
@ -48,14 +46,14 @@ export const SettingsObjectFieldDisabledActionDropdown = ({
LeftIcon={IconArchiveOff} LeftIcon={IconArchiveOff}
onClick={handleActivate} onClick={handleActivate}
/> />
{isCustomField && ( {/* {isCustomField && (
<MenuItem <MenuItem
text="Erase" text="Erase"
accent="danger" accent="danger"
LeftIcon={IconTrash} LeftIcon={IconTrash}
onClick={handleErase} onClick={handleErase}
/> />
)} )} */}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </DropdownMenu>
} }

View File

@ -1,4 +1,4 @@
import { IconDotsVertical, IconTrash } from '@/ui/display/icon'; import { IconDotsVertical } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { IconArchiveOff } from '@/ui/input/constants/icons'; import { IconArchiveOff } from '@/ui/input/constants/icons';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -16,9 +16,7 @@ type SettingsObjectDisabledMenuDropDownProps = {
}; };
export const SettingsObjectDisabledMenuDropDown = ({ export const SettingsObjectDisabledMenuDropDown = ({
isCustomObject,
onActivate, onActivate,
onErase,
scopeKey, scopeKey,
}: SettingsObjectDisabledMenuDropDownProps) => { }: SettingsObjectDisabledMenuDropDownProps) => {
const dropdownScopeId = `${scopeKey}-settings-object-disabled-menu-dropdown`; const dropdownScopeId = `${scopeKey}-settings-object-disabled-menu-dropdown`;
@ -30,10 +28,10 @@ export const SettingsObjectDisabledMenuDropDown = ({
closeDropdown(); closeDropdown();
}; };
const handleErase = () => { // const handleErase = () => {
onErase(); // onErase();
closeDropdown(); // closeDropdown();
}; // };
return ( return (
<DropdownScope dropdownScopeId={dropdownScopeId}> <DropdownScope dropdownScopeId={dropdownScopeId}>
@ -49,14 +47,14 @@ export const SettingsObjectDisabledMenuDropDown = ({
LeftIcon={IconArchiveOff} LeftIcon={IconArchiveOff}
onClick={handleActivate} onClick={handleActivate}
/> />
{isCustomObject && ( {/* {isCustomObject && (
<MenuItem <MenuItem
text="Erase" text="Erase"
LeftIcon={IconTrash} LeftIcon={IconTrash}
accent="danger" accent="danger"
onClick={handleErase} onClick={handleErase}
/> />
)} )} */}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </DropdownMenu>
} }

View File

@ -0,0 +1,6 @@
import { useLocation } from 'react-router-dom';
export const useIsMenuNavbarDisplayed = () => {
const currentPath = useLocation().pathname;
return currentPath.match(/^\/companies(\/.*)?$/) !== null;
};

View File

@ -1,6 +0,0 @@
import { useLocation } from 'react-router-dom';
export const useIsSubMenuNavbarDisplayed = () => {
const currentPath = useLocation().pathname;
return currentPath.match(/\/settings\//g) !== null;
};

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { AnimatePresence, LayoutGroup } from 'framer-motion'; import { AnimatePresence, LayoutGroup } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { AuthModal } from '@/auth/components/Modal'; import { AuthModal } from '@/auth/components/Modal';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
@ -8,23 +8,22 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { CommandMenu } from '@/command-menu/components/CommandMenu'; import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu'; import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
import { DesktopNavigationDrawer } from '@/navigation/components/DesktopNavigationDrawer';
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
import { MobileNavigationDrawer } from '@/navigation/components/MobileNavigationDrawer';
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer'; import { NavbarAnimatedContainer } from '@/ui/navigation/navigation-drawer/components/NavbarAnimatedContainer';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { AppNavbar } from '~/AppNavbar';
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
const StyledLayout = styled.div` const StyledLayout = styled.div`
background: ${({ theme }) => theme.background.noisy}; background: ${({ theme }) => theme.background.noisy};
display: flex; display: flex;
flex-direction: row; flex-direction: column;
height: 100vh; height: 100vh;
position: relative; position: relative;
scrollbar-color: ${({ theme }) => theme.border.color.medium}; scrollbar-color: ${({ theme }) => theme.border.color.medium};
scrollbar-width: 4px; scrollbar-width: 4px;
width: 100vw; width: 100%;
*::-webkit-scrollbar { *::-webkit-scrollbar {
height: 4px; height: 4px;
@ -41,43 +40,51 @@ const StyledLayout = styled.div`
} }
`; `;
const StyledMainContainer = styled.div` const StyledPageContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: row; flex-direction: row;
`;
const StyledMainContainer = styled.div`
display: flex;
flex: 0 1 100%;
flex-direction: row;
overflow: hidden; overflow: hidden;
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: ${() => (useRecoilValue(isNavbarOpenedState) ? '0' : '100%')};
}
`; `;
type DefaultLayoutProps = { type DefaultLayoutProps = {
children: React.ReactNode; children: ReactNode;
}; };
export const DefaultLayout = ({ children }: DefaultLayoutProps) => { export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const isMobile = useIsMobile();
return ( return (
<StyledLayout> <StyledLayout>
<CommandMenu /> <CommandMenu />
<KeyboardShortcutMenu /> <KeyboardShortcutMenu />
<NavbarAnimatedContainer> <StyledPageContainer>
<AppNavbar /> <NavbarAnimatedContainer>
</NavbarAnimatedContainer> {isMobile ? <MobileNavigationDrawer /> : <DesktopNavigationDrawer />}
<StyledMainContainer> </NavbarAnimatedContainer>
{onboardingStatus && onboardingStatus !== OnboardingStatus.Completed ? ( <StyledMainContainer>
<> {onboardingStatus &&
<SignInBackgroundMockPage /> onboardingStatus !== OnboardingStatus.Completed ? (
<AnimatePresence mode="wait"> <>
<LayoutGroup> <SignInBackgroundMockPage />
<AuthModal>{children}</AuthModal> <AnimatePresence mode="wait">
</LayoutGroup> <LayoutGroup>
</AnimatePresence> <AuthModal>{children}</AuthModal>
</> </LayoutGroup>
) : ( </AnimatePresence>
<AppErrorBoundary>{children}</AppErrorBoundary> </>
)} ) : (
</StyledMainContainer> <AppErrorBoundary>{children}</AppErrorBoundary>
)}
</StyledMainContainer>
</StyledPageContainer>
{isMobile && <MobileNavigationBar />}
</StyledLayout> </StyledLayout>
); );
}; };

View File

@ -1,12 +1 @@
import { PAGE_BAR_MIN_HEIGHT } from './PageHeader'; export { RightDrawerContainer as PageBody } from './RightDrawerContainer';
import { RightDrawerContainer } from './RightDrawerContainer';
type PageBodyProps = {
children: JSX.Element | JSX.Element[];
};
export const PageBody = ({ children }: PageBodyProps) => (
<RightDrawerContainer topMargin={PAGE_BAR_MIN_HEIGHT + 16 + 16}>
{children}
</RightDrawerContainer>
);

View File

@ -1,15 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
type PageContainerProps = {
children: JSX.Element | JSX.Element[];
};
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
`; `;
export const PageContainer = ({ children }: PageContainerProps) => ( export { StyledContainer as PageContainer };
<StyledContainer>{children}</StyledContainer>
);

View File

@ -1,4 +1,4 @@
import { ComponentProps, useCallback } from 'react'; import { ComponentProps, ReactNode } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -7,15 +7,12 @@ import { useRecoilValue } from 'recoil';
import { IconChevronLeft } from '@/ui/display/icon/index'; import { IconChevronLeft } from '@/ui/display/icon/index';
import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { import { IconButton } from '@/ui/input/button/components/IconButton';
IconButton, import NavCollapseButton from '@/ui/navigation/navigation-drawer/components/NavCollapseButton';
IconButtonSize, import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
} from '@/ui/input/button/components/IconButton'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import NavCollapseButton from '@/ui/navigation/navbar/components/NavCollapseButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
export const PAGE_BAR_MIN_HEIGHT = 40; export const PAGE_BAR_MIN_HEIGHT = 40;
const StyledTopBarContainer = styled.div` const StyledTopBarContainer = styled.div`
@ -31,13 +28,23 @@ const StyledTopBarContainer = styled.div`
padding-left: 0; padding-left: 0;
padding-right: ${({ theme }) => theme.spacing(3)}; padding-right: ${({ theme }) => theme.spacing(3)};
z-index: 20; z-index: 20;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(3)};
}
`; `;
const StyledLeftContainer = styled.div` const StyledLeftContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
width: 100%; width: 100%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(1)};
}
`; `;
const StyledTitleContainer = styled.div` const StyledTitleContainer = styled.div`
@ -47,24 +54,15 @@ const StyledTitleContainer = styled.div`
max-width: 50%; max-width: 50%;
`; `;
const StyledTopBarButtonContainer = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledBackIconButton = styled(IconButton)` const StyledBackIconButton = styled(IconButton)`
margin-right: ${({ theme }) => theme.spacing(1)}; margin-right: ${({ theme }) => theme.spacing(1)};
`; `;
const StyledTopBarIconStyledTitleContainer = styled.div<{ const StyledTopBarIconStyledTitleContainer = styled.div`
hideLeftPadding?: boolean;
}>`
align-items: center; align-items: center;
display: flex; display: flex;
flex: 1 0 100%;
flex-direction: row; flex-direction: row;
padding-left: ${({ theme, hideLeftPadding }) =>
hideLeftPadding ? theme.spacing(2) : undefined};
width: 100%;
`; `;
const StyledPageActionContainer = styled.div` const StyledPageActionContainer = styled.div`
@ -72,11 +70,16 @@ const StyledPageActionContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledTopBarButtonContainer = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type PageHeaderProps = ComponentProps<'div'> & { type PageHeaderProps = ComponentProps<'div'> & {
title: string; title: string;
hasBackButton?: boolean; hasBackButton?: boolean;
Icon: IconComponent; Icon: IconComponent;
children?: JSX.Element | JSX.Element[]; children?: ReactNode;
}; };
export const PageHeader = ({ export const PageHeader = ({
@ -85,33 +88,28 @@ export const PageHeader = ({
Icon, Icon,
children, children,
}: PageHeaderProps) => { }: PageHeaderProps) => {
const isMobile = useIsMobile();
const navigate = useNavigate(); const navigate = useNavigate();
const navigateBack = useCallback(() => navigate(-1), [navigate]);
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
const iconSize: IconButtonSize = useIsMobile() ? 'small' : 'medium';
const theme = useTheme(); const theme = useTheme();
const navigationDrawer = useRecoilValue(navigationDrawerState);
return ( return (
<StyledTopBarContainer> <StyledTopBarContainer>
<StyledLeftContainer> <StyledLeftContainer>
{!isNavbarOpened && ( {navigationDrawer === '' && (
<StyledTopBarButtonContainer> <StyledTopBarButtonContainer>
<NavCollapseButton direction="right" /> <NavCollapseButton direction="right" />
</StyledTopBarButtonContainer> </StyledTopBarButtonContainer>
)} )}
{hasBackButton && ( {hasBackButton && (
<StyledTopBarButtonContainer> <StyledBackIconButton
<StyledBackIconButton Icon={IconChevronLeft}
Icon={IconChevronLeft} size={isMobile ? 'small' : 'medium'}
size={iconSize} onClick={() => navigate(-1)}
onClick={navigateBack} variant="tertiary"
variant="tertiary" />
/>
</StyledTopBarButtonContainer>
)} )}
<StyledTopBarIconStyledTitleContainer hideLeftPadding={!hasBackButton}> <StyledTopBarIconStyledTitleContainer>
{Icon && <Icon size={theme.icon.size.md} />} {Icon && <Icon size={theme.icon.size.md} />}
<StyledTitleContainer data-testid="top-bar-title"> <StyledTitleContainer data-testid="top-bar-title">
<OverflowingTextWithTooltip text={title} /> <OverflowingTextWithTooltip text={title} />

View File

@ -1,25 +1,30 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer'; import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { PagePanel } from './PagePanel'; import { PagePanel } from './PagePanel';
type RightDrawerContainerProps = { type RightDrawerContainerProps = {
children: JSX.Element | JSX.Element[]; children: ReactNode;
topMargin?: number;
}; };
const StyledMainContainer = styled.div<{ topMargin: number }>` const StyledMainContainer = styled.div`
background: ${({ theme }) => theme.background.noisy}; background: ${({ theme }) => theme.background.noisy};
box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
height: calc(100% - ${(props) => props.topMargin}px); height: 100%;
padding-bottom: ${({ theme }) => theme.spacing(3)}; padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(3)}; padding-right: ${({ theme }) => theme.spacing(3)};
width: calc(100% - ${({ theme }) => theme.spacing(3)}); width: 100%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(3)};
padding-bottom: 0;
}
`; `;
type LeftContainerProps = { type LeftContainerProps = {
@ -35,9 +40,8 @@ const StyledLeftContainer = styled.div<LeftContainerProps>`
export const RightDrawerContainer = ({ export const RightDrawerContainer = ({
children, children,
topMargin,
}: RightDrawerContainerProps) => ( }: RightDrawerContainerProps) => (
<StyledMainContainer topMargin={topMargin ?? 0}> <StyledMainContainer>
<StyledLeftContainer> <StyledLeftContainer>
<PagePanel>{children}</PagePanel> <PagePanel>{children}</PagePanel>
</StyledLeftContainer> </StyledLeftContainer>

View File

@ -30,7 +30,7 @@ export const SubMenuTopBarContainer = ({
return ( return (
<StyledContainer isMobile={isMobile}> <StyledContainer isMobile={isMobile}>
{isMobile && <PageHeader title={title} Icon={Icon} />} {isMobile && <PageHeader title={title} Icon={Icon} />}
<RightDrawerContainer topMargin={16}>{children}</RightDrawerContainer> <RightDrawerContainer>{children}</RightDrawerContainer>
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -13,7 +13,6 @@ import {
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { leftNavbarWidth } from '../../../navigation/navbar/constants';
import { useRightDrawer } from '../hooks/useRightDrawer'; import { useRightDrawer } from '../hooks/useRightDrawer';
import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState'; import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
@ -70,15 +69,9 @@ export const RightDrawer = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const rightDrawerWidthExpanded = `calc(100% - ${
leftNavbarWidth.desktop
} - ${theme.spacing(2)})`;
const rightDrawerWidth = isRightDrawerOpen const rightDrawerWidth = isRightDrawerOpen
? isMobile ? isMobile || isRightDrawerExpanded
? '100%' ? '100%'
: isRightDrawerExpanded
? rightDrawerWidthExpanded
: theme.rightDrawerWidth : theme.rightDrawerWidth
: '0'; : '0';

View File

@ -1,10 +0,0 @@
import { atom } from 'recoil';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
const isMobile = window.innerWidth <= MOBILE_VIEWPORT;
export const isNavbarOpenedState = atom({
key: 'ui/isNavbarOpenedState',
default: !isMobile,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const isNavbarSwitchingSizeState = atom({
key: 'ui/isNavbarSwitchingSizeState',
default: true,
});

View File

@ -1,63 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useIsSubMenuNavbarDisplayed } from '@/ui/layout/hooks/useIsSubMenuNavbarDisplayed';
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { leftNavbarWidth, leftSubMenuNavbarWidth } from '../constants';
const StyledNavbarContainer = styled(motion.div)`
align-items: end;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(2)};
`;
type NavbarAnimatedContainerProps = {
children: React.ReactNode;
};
export const NavbarAnimatedContainer = ({
children,
}: NavbarAnimatedContainerProps) => {
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
const [, setIsNavbarSwitchingSize] = useRecoilState(
isNavbarSwitchingSizeState,
);
const isInSubMenu = useIsSubMenuNavbarDisplayed();
const theme = useTheme();
const isMobile = useIsMobile();
const leftBarWidth = isInSubMenu
? isMobile
? leftSubMenuNavbarWidth.mobile
: leftSubMenuNavbarWidth.desktop
: isMobile
? leftNavbarWidth.mobile
: leftNavbarWidth.desktop;
return (
<StyledNavbarContainer
onAnimationComplete={() => {
setIsNavbarSwitchingSize(false);
}}
initial={false}
animate={{
width: isNavbarOpened ? leftBarWidth : '0',
opacity: isNavbarOpened ? 1 : 0,
}}
transition={{
duration: theme.animation.duration.normal,
}}
>
{children}
</StyledNavbarContainer>
);
};

View File

@ -1,11 +0,0 @@
export const leftNavbarWidth = {
mobile: 'calc(100% - 16px)',
desktop: '220px',
};
export const leftSubMenuNavbarWidth = {
mobile: 'calc(100% - 16px)',
desktop: '520px',
};
export const githubLink = 'https://github.com/twentyhq/twenty';

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { NavigationBarItem } from './NavigationBarItem';
const StyledContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(3)};
`;
type NavigationBarProps = {
activeItemName: string;
items: { name: string; Icon: IconComponent; onClick: () => void }[];
};
export const NavigationBar = ({
activeItemName,
items,
}: NavigationBarProps) => (
<StyledContainer>
{items.map(({ Icon, name, onClick }) => (
<NavigationBarItem
key={name}
Icon={Icon}
isActive={activeItemName === name}
onClick={onClick}
/>
))}
</StyledContainer>
);

View File

@ -0,0 +1,42 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
const StyledIconButton = styled.div<{ isActive?: boolean }>`
align-items: center;
background-color: ${({ isActive, theme }) =>
isActive ? theme.background.transparent.light : 'none'};
border-radius: ${({ theme }) => theme.spacing(1)};
cursor: pointer;
display: flex;
height: ${({ theme }) => theme.spacing(10)};
justify-content: center;
transition: background-color ${({ theme }) => theme.animation.duration.fast}s
ease;
width: ${({ theme }) => theme.spacing(10)};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
type NavigationBarItemProps = {
Icon: IconComponent;
isActive: boolean;
onClick: () => void;
};
export const NavigationBarItem = ({
Icon,
isActive,
onClick,
}: NavigationBarItemProps) => {
const theme = useTheme();
return (
<StyledIconButton isActive={isActive} onClick={onClick}>
<Icon color={theme.color.gray50} size={theme.icon.size.lg} />
</StyledIconButton>
);
};

View File

@ -0,0 +1,33 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { NavigationBar } from '../NavigationBar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
IconList,
IconSearch,
IconCheckbox,
IconSettings,
} from '@/ui/display/icon';
const meta: Meta<typeof NavigationBar> = {
title: 'UI/Navigation/NavigationBar/NavigationBar',
component: NavigationBar,
args: {
activeItemName: 'main',
items: [
{ name: 'main', Icon: IconList, onClick: () => undefined },
{ name: 'search', Icon: IconSearch, onClick: () => undefined },
{ name: 'tasks', Icon: IconCheckbox, onClick: () => undefined },
{ name: 'settings', Icon: IconSettings, onClick: () => undefined },
],
},
};
export default meta;
type Story = StoryObj<typeof NavigationBar>;
export const Default: Story = {
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
};

View File

@ -10,11 +10,13 @@ type MainNavbarProps = {
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
justify-content: space-between; justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(2.5)}; margin-bottom: ${({ theme }) => theme.spacing(2.5)};
padding: ${({ theme }) => theme.spacing(2)};
width: 100%; width: 100%;
`; `;

View File

@ -1,9 +1,9 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconChevronLeft } from '@/ui/display/icon/index'; import { IconChevronLeft } from '@/ui/display/icon/index';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
type NavBackButtonProps = { type NavBackButtonProps = {
title: string; title: string;
@ -32,24 +32,19 @@ const StyledContainer = styled.div`
const NavBackButton = ({ title }: NavBackButtonProps) => { const NavBackButton = ({ title }: NavBackButtonProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [, setIsNavbarSwitchingSize] = useRecoilState( const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
isNavbarSwitchingSizeState,
);
return ( return (
<> <StyledContainer>
<StyledContainer> <StyledIconAndButtonContainer
<StyledIconAndButtonContainer onClick={() => {
onClick={() => { navigate(navigationMemorizedUrl, { replace: true });
setIsNavbarSwitchingSize(true); }}
navigate('/', { replace: true }); >
}} <IconChevronLeft />
> <span>{title}</span>
<IconChevronLeft /> </StyledIconAndButtonContainer>
<span>{title}</span> </StyledContainer>
</StyledIconAndButtonContainer>
</StyledContainer>
</>
); );
}; };

View File

@ -1,14 +1,14 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { import {
IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse, IconLayoutSidebarRightCollapse,
} from '@/ui/display/icon'; } from '@/ui/display/icon';
import { IconButton } from '@/ui/input/button/components/IconButton'; import { IconButton } from '@/ui/input/button/components/IconButton';
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState'; import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
const StyledCollapseButton = styled(motion.div)` const StyledCollapseButton = styled(motion.div)`
align-items: center; align-items: center;
@ -41,8 +41,7 @@ const NavCollapseButton = ({
direction = 'left', direction = 'left',
show = true, show = true,
}: NavCollapseButtonProps) => { }: NavCollapseButtonProps) => {
const [isNavbarOpened, setIsNavbarOpened] = const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
useRecoilState(isNavbarOpenedState);
const iconSize = 'small'; const iconSize = 'small';
const theme = useTheme(); const theme = useTheme();
@ -57,7 +56,11 @@ const NavCollapseButton = ({
transition={{ transition={{
duration: theme.animation.duration.normal, duration: theme.animation.duration.normal,
}} }}
onClick={() => setIsNavbarOpened(!isNavbarOpened)} onClick={() =>
setNavigationDrawer((navigationDrawer) =>
navigationDrawer === '' ? 'main' : '',
)
}
> >
<IconButton <IconButton
Icon={ Icon={

View File

@ -1,15 +1,15 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNavbarOpenedState } from '../../../layout/states/isNavbarOpenedState';
type NavItemProps = { type NavItemProps = {
className?: string;
label: string; label: string;
to?: string; to?: string;
onClick?: () => void; onClick?: () => void;
@ -115,6 +115,7 @@ const StyledKeyBoardShortcut = styled.div`
`; `;
const NavItem = ({ const NavItem = ({
className,
label, label,
Icon, Icon,
to, to,
@ -126,25 +127,26 @@ const NavItem = ({
keyboard, keyboard,
}: NavItemProps) => { }: NavItemProps) => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate();
const [, setIsNavbarOpened] = useRecoilState(isNavbarOpenedState);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const navigate = useNavigate();
const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
const handleItemClick = () => { const handleItemClick = () => {
if (isMobile) { if (isMobile) {
setIsNavbarOpened(false); setNavigationDrawer('');
} }
if (onClick) { if (onClick) {
onClick(); onClick();
} else if (to) { return;
navigate(to);
} }
if (to) navigate(to);
}; };
return ( return (
<StyledItem <StyledItem
className={className}
onClick={handleItemClick} onClick={handleItemClick}
active={active} active={active}
aria-selected={active} aria-selected={active}
@ -157,7 +159,7 @@ const NavItem = ({
{!!count && <StyledItemCount>{count}</StyledItemCount>} {!!count && <StyledItemCount>{count}</StyledItemCount>}
{keyboard && ( {keyboard && (
<StyledKeyBoardShortcut className="keyboard-shortcuts"> <StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard.map((key) => key)} {keyboard}
</StyledKeyBoardShortcut> </StyledKeyBoardShortcut>
)} )}
</StyledItem> </StyledItem>

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI'; import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import NavCollapseButton from './NavCollapseButton'; import NavCollapseButton from './NavCollapseButton';
@ -53,7 +54,7 @@ const NavWorkspaceButton = ({
showCollapseButton, showCollapseButton,
}: NavWorkspaceButtonProps) => { }: NavWorkspaceButtonProps) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isMobile = useIsMobile();
const DEFAULT_LOGO = const DEFAULT_LOGO =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII='; 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
@ -69,7 +70,9 @@ const NavWorkspaceButton = ({
></StyledLogo> ></StyledLogo>
<StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName> <StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName>
</StyledLogoAndNameContainer> </StyledLogoAndNameContainer>
<NavCollapseButton direction="left" show={showCollapseButton} /> {!isMobile && (
<NavCollapseButton direction="left" show={showCollapseButton} />
)}
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -0,0 +1,55 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { desktopNavDrawerWidths } from '../constants';
const StyledNavbarContainer = styled(motion.div)`
align-items: end;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
`;
type NavbarAnimatedContainerProps = {
children: ReactNode;
};
export const NavbarAnimatedContainer = ({
children,
}: NavbarAnimatedContainerProps) => {
const navigationDrawer = useRecoilValue(navigationDrawerState);
const isInSubMenu = useIsSettingsPage();
const theme = useTheme();
const isMobile = useIsMobile();
const desktopWidth =
navigationDrawer === ''
? 12
: isInSubMenu
? desktopNavDrawerWidths.submenu
: desktopNavDrawerWidths.menu;
return (
<StyledNavbarContainer
initial={false}
animate={{
width: !isMobile ? desktopWidth : navigationDrawer ? '100%' : 0,
opacity: navigationDrawer === '' ? 0 : 1,
}}
transition={{
duration: theme.animation.duration.normal,
}}
>
{children}
</StyledNavbarContainer>
);
};

View File

@ -1,17 +1,19 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconBrandGithub } from '@/ui/display/icon'; import { IconBrandGithub } from '@/ui/display/icon';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import packageJson from '../../../../../../package.json'; import packageJson from '../../../../../../package.json';
import { githubLink, leftNavbarWidth } from '../constants'; import { desktopNavDrawerWidths, githubLink } from '../constants';
import NavBackButton from './NavBackButton'; import NavBackButton from './NavBackButton';
import NavItemsContainer from './NavItemsContainer'; import NavItemsContainer from './NavItemsContainer';
type SubMenuNavbarProps = { type SubMenuNavbarProps = {
children: React.ReactNode; children: ReactNode;
backButtonTitle: string; backButtonTitle: string;
displayVersion?: boolean; displayVersion?: boolean;
}; };
@ -25,10 +27,11 @@ const StyledVersionContainer = styled.div`
const StyledVersion = styled.span` const StyledVersion = styled.span`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
padding-left: ${({ theme }) => theme.spacing(1)};
:hover { :hover {
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
} }
padding-left: ${({ theme }) => theme.spacing(1)};
`; `;
const StyledVersionLink = styled.a` const StyledVersionLink = styled.a`
@ -36,18 +39,25 @@ const StyledVersionLink = styled.a`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
display: flex; display: flex;
text-decoration: none; text-decoration: none;
:hover { :hover {
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
} }
`; `;
const StyledContainer = styled.div` const StyledContainer = styled.div`
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
justify-content: space-between; justify-content: space-between;
padding-top: ${({ theme }) => theme.spacing(9)}; padding: ${({ theme }) => theme.spacing(2)};
width: ${() => (useIsMobile() ? '100%' : leftNavbarWidth.desktop)}; padding-top: ${({ theme }) => theme.spacing(11)};
width: ${desktopNavDrawerWidths.menu};
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 100%;
}
`; `;
const SubMenuNavbar = ({ const SubMenuNavbar = ({
@ -56,13 +66,14 @@ const SubMenuNavbar = ({
displayVersion, displayVersion,
}: SubMenuNavbarProps) => { }: SubMenuNavbarProps) => {
const version = packageJson.version; const version = packageJson.version;
const isMobile = useIsMobile();
const theme = useTheme(); const theme = useTheme();
return ( return (
<StyledContainer> <StyledContainer>
<div> <div>
<NavBackButton title={backButtonTitle} /> {!isMobile && <NavBackButton title={backButtonTitle} />}
<NavItemsContainer>{children}</NavItemsContainer> <NavItemsContainer>{children}</NavItemsContainer>
</div> </div>
{displayVersion && ( {displayVersion && (

View File

@ -18,7 +18,7 @@ import NavItem from '../NavItem';
import NavTitle from '../NavTitle'; import NavTitle from '../NavTitle';
const meta: Meta<typeof MainNavbar> = { const meta: Meta<typeof MainNavbar> = {
title: 'UI/Navigation/Navbar/MainNavbar', title: 'UI/Navigation/NavigationDrawer/MainNavbar',
component: MainNavbar, component: MainNavbar,
decorators: [SnackBarDecorator], decorators: [SnackBarDecorator],
}; };

View File

@ -5,7 +5,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import NavCollapseButton from '../NavCollapseButton'; import NavCollapseButton from '../NavCollapseButton';
const meta: Meta<typeof NavCollapseButton> = { const meta: Meta<typeof NavCollapseButton> = {
title: 'UI/Navigation/Navbar/NavCollapseButton', title: 'UI/Navigation/NavigationDrawer/NavCollapseButton',
component: NavCollapseButton, component: NavCollapseButton,
}; };

View File

@ -9,8 +9,14 @@ import { CatalogStory } from '~/testing/types';
import NavItem from '../NavItem'; import NavItem from '../NavItem';
const meta: Meta<typeof NavItem> = { const meta: Meta<typeof NavItem> = {
title: 'UI/Navigation/Navbar/NavItem', title: 'UI/Navigation/NavigationDrawer/NavItem',
component: NavItem, component: NavItem,
args: {
label: 'Search',
Icon: IconSearch,
active: true,
},
argTypes: { Icon: { control: false } },
}; };
const StyledNavItemContainer = styled.div` const StyledNavItemContainer = styled.div`
@ -28,19 +34,11 @@ const ComponentDecorator: Decorator = (Story) => (
export default meta; export default meta;
type Story = StoryObj<typeof NavItem>; type Story = StoryObj<typeof NavItem>;
export const Default: Story = { export const Default: Story = {
args: {
label: 'Search',
Icon: IconSearch,
onClick: () => console.log('clicked'),
active: true,
},
argTypes: { Icon: { control: false }, onClick: { control: false } },
decorators: [ComponentDecorator, ComponentWithRouterDecorator], decorators: [ComponentDecorator, ComponentWithRouterDecorator],
}; };
export const Catalog: CatalogStory<Story, typeof NavItem> = { export const Catalog: CatalogStory<Story, typeof NavItem> = {
args: Default.args,
decorators: [ decorators: [
ComponentDecorator, ComponentDecorator,
CatalogDecorator, CatalogDecorator,
@ -75,21 +73,28 @@ export const Catalog: CatalogStory<Story, typeof NavItem> = {
}, },
}; };
export const Soon: Story = { export const WithSoonPill: Story = {
...Default,
args: { args: {
...Default.args,
active: false, active: false,
soon: true, soon: true,
}, },
argTypes: { Icon: { control: false }, onClick: { control: false } },
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
}; };
export const Count: Story = { export const WithCount: Story = {
...Default,
args: { args: {
...Default.args,
count: 3, count: 3,
}, },
argTypes: { Icon: { control: false }, onClick: { control: false } }, };
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
export const WithKeyboardKeys: Story = {
...Default,
args: {
className: "hover",
keyboard: ['⌘', 'K'],
},
parameters: {
pseudo: { hover: [".hover"] },
}
}; };

View File

@ -14,7 +14,7 @@ import NavTitle from '../NavTitle';
import SubMenuNavbar from '../SubMenuNavbar'; import SubMenuNavbar from '../SubMenuNavbar';
const meta: Meta<typeof SubMenuNavbar> = { const meta: Meta<typeof SubMenuNavbar> = {
title: 'UI/Navigation/Navbar/SubMenuNavbar', title: 'UI/Navigation/NavigationDrawer/SubMenuNavbar',
component: SubMenuNavbar, component: SubMenuNavbar,
}; };

View File

@ -0,0 +1,6 @@
export const desktopNavDrawerWidths = {
menu: '236px',
submenu: '536px',
};
export const githubLink = 'https://github.com/twentyhq/twenty';

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const navigationDrawerState = atom<'main' | 'settings' | ''>({
key: 'ui/navigationDrawerState',
default: 'main',
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const navigationMemorizedUrlState = atom<string>({
key: 'navigationMemorizedUrlState',
default: '/',
});

View File

@ -6,16 +6,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable'; import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import {
RecordTableRow,
StyledRow,
} from '@/ui/object/record-table/components/RecordTableRow';
import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext';
import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext';
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { RowIdContext } from '../contexts/RowIdContext';
import { RowIndexContext } from '../contexts/RowIndexContext';
import { useRecordTable } from '../hooks/useRecordTable'; import { useRecordTable } from '../hooks/useRecordTable';
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
import { tableRowIdsState } from '../states/tableRowIdsState'; import { tableRowIdsState } from '../states/tableRowIdsState';
import { RecordTableRow, StyledRow } from './RecordTableRow';
export const RecordTableBody = () => { export const RecordTableBody = () => {
const { ref: lastTableRowRef, inView: lastTableRowIsVisible } = useInView(); const { ref: lastTableRowRef, inView: lastTableRowIsVisible } = useInView();
@ -41,6 +43,7 @@ export const RecordTableBody = () => {
isFetchingRecordTableDataState, isFetchingRecordTableDataState,
); );
// Todo, move this to an effect to not trigger many re-renders
const { fetchMoreRecords: fetchMoreObjects } = useObjectRecordTable(); const { fetchMoreRecords: fetchMoreObjects } = useObjectRecordTable();
useEffect(() => { useEffect(() => {

View File

@ -1,12 +0,0 @@
/* eslint-disable no-console */
import afterFrame from 'afterframe';
export const measureTotalFrameLoad = (id: string) => {
const timerId = `Total loading time for : ${id}`;
console.time(timerId);
afterFrame(() => {
console.timeEnd(timerId);
});
};