feat - Compact sidebar (#7414)

This commit is contained in:
nitin 2024-10-15 17:32:28 +05:30 committed by GitHub
parent c0610419c2
commit a9deede9ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 524 additions and 292 deletions

View File

@ -4,7 +4,7 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui'; import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader'; import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader';
@ -47,14 +47,14 @@ const StyledSkeletonTitleContainer = styled.div`
export const LeftPanelSkeletonLoader = () => { export const LeftPanelSkeletonLoader = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const mobileWidth = isMobile ? 0 : '100%';
const desktopWidth = !mobileWidth ? 12 : DESKTOP_NAV_DRAWER_WIDTHS.menu;
return ( return (
<StyledAnimatedContainer <StyledAnimatedContainer
initial={false} initial={false}
animate={{ animate={{
width: isMobile ? mobileWidth : desktopWidth, width: isMobile
? NAV_DRAWER_WIDTHS.menu.mobile.collapsed
: NAV_DRAWER_WIDTHS.menu.desktop.expanded,
opacity: isMobile ? 0 : 1, opacity: isMobile ? 0 : 1,
}} }}
transition={{ transition={{

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { BACKGROUND_LIGHT, MOBILE_VIEWPORT } from 'twenty-ui'; import { BACKGROUND_LIGHT, MOBILE_VIEWPORT } from 'twenty-ui';
import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader'; import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader'; import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
@ -12,7 +12,7 @@ const StyledContainer = styled.div`
flex-direction: row; flex-direction: row;
gap: 12px; gap: 12px;
height: 100dvh; height: 100dvh;
min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px; min-width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px;
width: 100%; width: 100%;
padding: 12px 8px 12px 8px; padding: 12px 8px 12px 8px;
overflow: hidden; overflow: hidden;

View File

@ -13,6 +13,8 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFavorites } from '../hooks/useFavorites'; import { useFavorites } from '../hooks/useFavorites';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
const StyledContainer = styled(NavigationDrawerSection)` const StyledContainer = styled(NavigationDrawerSection)`
width: 100%; width: 100%;
@ -39,7 +41,6 @@ export const CurrentWorkspaceMemberFavorites = () => {
const { favorites, handleReorderFavorite } = useFavorites(); const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading(); const loading = useIsPrefetchLoading();
const { toggleNavigationSection, isNavigationSectionOpenState } = const { toggleNavigationSection, isNavigationSectionOpenState } =
useNavigationSection('Favorites'); useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
@ -58,54 +59,65 @@ export const CurrentWorkspaceMemberFavorites = () => {
) )
return <></>; return <></>;
const isGroup = currentWorkspaceMemberFavorites.length > 1;
const draggableListContent = (
<DraggableList
onDragEnd={handleReorderFavorite}
draggableItems={
<>
{currentWorkspaceMemberFavorites.map((favorite, index) => {
const {
id,
labelIdentifier,
avatarUrl,
avatarType,
link,
recordId,
} = favorite;
return (
<DraggableItem
key={id}
draggableId={id}
index={index}
itemComponent={
<StyledNavigationDrawerItem
key={id}
label={labelIdentifier}
Icon={() => (
<StyledAvatar
placeholderColorSeed={recordId}
avatarUrl={avatarUrl}
type={avatarType}
placeholder={labelIdentifier}
className="fav-avatar"
/>
)}
to={link}
/>
}
/>
);
})}
</>
}
/>
);
return ( return (
<StyledContainer> <StyledContainer>
<NavigationDrawerSectionTitle <NavigationDrawerAnimatedCollapseWrapper>
label="Favorites" <NavigationDrawerSectionTitle
onClick={() => toggleNavigationSection()} label="Favorites"
/> onClick={() => toggleNavigationSection()}
{isNavigationSectionOpen && (
<DraggableList
onDragEnd={handleReorderFavorite}
draggableItems={
<>
{currentWorkspaceMemberFavorites.map((favorite, index) => {
const {
id,
labelIdentifier,
avatarUrl,
avatarType,
link,
recordId,
} = favorite;
return (
<DraggableItem
key={id}
draggableId={id}
index={index}
itemComponent={
<StyledNavigationDrawerItem
key={id}
label={labelIdentifier}
Icon={() => (
<StyledAvatar
placeholderColorSeed={recordId}
avatarUrl={avatarUrl}
type={avatarType}
placeholder={labelIdentifier}
className="fav-avatar"
/>
)}
to={link}
/>
}
/>
);
})}
</>
}
/> />
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
{draggableListContent}
</NavigationDrawerItemsCollapsedContainer>
)} )}
</StyledContainer> </StyledContainer>
); );

View File

@ -8,14 +8,14 @@ import {
NavigationDrawer, NavigationDrawer,
NavigationDrawerProps, NavigationDrawerProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer'; } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState';
import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle'; import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { MainNavigationDrawerItems } from './MainNavigationDrawerItems'; import { MainNavigationDrawerItems } from './MainNavigationDrawerItems';
export type AppNavigationDrawerProps = { export type AppNavigationDrawerProps = {
@ -26,22 +26,14 @@ export const AppNavigationDrawer = ({
className, className,
}: AppNavigationDrawerProps) => { }: AppNavigationDrawerProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isSettingsPage = useIsSettingsPage(); const isSettingsDrawer = useIsSettingsDrawer();
const currentMobileNavigationDrawer = useRecoilValue( const setIsNavigationDrawerExpanded = useSetRecoilState(
currentMobileNavigationDrawerState, isNavigationDrawerExpandedState,
);
const setIsNavigationDrawerOpen = useSetRecoilState(
isNavigationDrawerOpenState,
); );
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isSettingsDrawer = isMobile
? currentMobileNavigationDrawer === 'settings'
: isSettingsPage;
const drawerProps: NavigationDrawerProps = isSettingsDrawer const drawerProps: NavigationDrawerProps = isSettingsDrawer
? { ? {
isSubMenu: true,
title: 'Exit Settings', title: 'Exit Settings',
children: <SettingsNavigationDrawerItems />, children: <SettingsNavigationDrawerItems />,
footer: <AdvancedSettingsToggle />, footer: <AdvancedSettingsToggle />,
@ -57,13 +49,12 @@ export const AppNavigationDrawer = ({
}; };
useEffect(() => { useEffect(() => {
setIsNavigationDrawerOpen(!isMobile); setIsNavigationDrawerExpanded(!isMobile);
}, [isMobile, setIsNavigationDrawerOpen]); }, [isMobile, setIsNavigationDrawerExpanded]);
return ( return (
<NavigationDrawer <NavigationDrawer
className={className} className={className}
isSubMenu={drawerProps.isSubMenu}
logo={drawerProps.logo} logo={drawerProps.logo}
title={drawerProps.title} title={drawerProps.title}
footer={drawerProps.footer} footer={drawerProps.footer}

View File

@ -1,5 +1,5 @@
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { IconSearch, IconSettings } from 'twenty-ui'; import { IconSearch, IconSettings } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
@ -9,6 +9,8 @@ import { NavigationDrawerOpenedSection } from '@/object-metadata/components/Navi
import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper'; import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -23,6 +25,11 @@ export const MainNavigationDrawerItems = () => {
const isWorkspaceFavoriteEnabled = useIsFeatureEnabled( const isWorkspaceFavoriteEnabled = useIsFeatureEnabled(
'IS_WORKSPACE_FAVORITE_ENABLED', 'IS_WORKSPACE_FAVORITE_ENABLED',
); );
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilState(
navigationDrawerExpandedMemorizedState,
);
return ( return (
<> <>
@ -38,6 +45,8 @@ export const MainNavigationDrawerItems = () => {
label="Settings" label="Settings"
to={'/settings/profile'} to={'/settings/profile'}
onClick={() => { onClick={() => {
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
setIsNavigationDrawerExpanded(true);
setNavigationMemorizedUrl(location.pathname + location.search); setNavigationMemorizedUrl(location.pathname + location.search);
}} }}
Icon={IconSettings} Icon={IconSettings}

View File

@ -1,11 +1,9 @@
import { useRecoilState } from 'recoil';
import { IconComponent, IconList, IconSearch, IconSettings } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar'; import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilState } from 'recoil';
import { IconComponent, IconList, IconSearch, IconSettings } from 'twenty-ui';
import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState';
@ -15,13 +13,12 @@ export const MobileNavigationBar = () => {
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState); const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
const { closeCommandMenu, openCommandMenu } = useCommandMenu(); const { closeCommandMenu, openCommandMenu } = useCommandMenu();
const isSettingsPage = useIsSettingsPage(); const isSettingsPage = useIsSettingsPage();
const [isNavigationDrawerOpen, setIsNavigationDrawerOpen] = useRecoilState( const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
isNavigationDrawerOpenState, useRecoilState(isNavigationDrawerExpandedState);
);
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] = const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
useRecoilState(currentMobileNavigationDrawerState); useRecoilState(currentMobileNavigationDrawerState);
const activeItemName = isNavigationDrawerOpen const activeItemName = isNavigationDrawerExpanded
? currentMobileNavigationDrawer ? currentMobileNavigationDrawer
: isCommandMenuOpened : isCommandMenuOpened
? 'search' ? 'search'
@ -39,7 +36,7 @@ export const MobileNavigationBar = () => {
Icon: IconList, Icon: IconList,
onClick: () => { onClick: () => {
closeCommandMenu(); closeCommandMenu();
setIsNavigationDrawerOpen( setIsNavigationDrawerExpanded(
(previousIsOpen) => activeItemName !== 'main' || !previousIsOpen, (previousIsOpen) => activeItemName !== 'main' || !previousIsOpen,
); );
setCurrentMobileNavigationDrawer('main'); setCurrentMobileNavigationDrawer('main');
@ -52,7 +49,7 @@ export const MobileNavigationBar = () => {
if (!isCommandMenuOpened) { if (!isCommandMenuOpened) {
openCommandMenu(); openCommandMenu();
} }
setIsNavigationDrawerOpen(false); setIsNavigationDrawerExpanded(false);
}, },
}, },
{ {
@ -60,7 +57,7 @@ export const MobileNavigationBar = () => {
Icon: IconSettings, Icon: IconSettings,
onClick: () => { onClick: () => {
closeCommandMenu(); closeCommandMenu();
setIsNavigationDrawerOpen( setIsNavigationDrawerExpanded(
(previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen, (previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen,
); );
setCurrentMobileNavigationDrawer('settings'); setCurrentMobileNavigationDrawer('settings');

View File

@ -1,16 +1,17 @@
import { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState'; import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { import {
AppNavigationDrawer, AppNavigationDrawer,
AppNavigationDrawerProps, AppNavigationDrawerProps,
@ -22,8 +23,8 @@ const MobileNavigationDrawerStateSetterEffect = ({
mobileNavigationDrawer?: 'main' | 'settings'; mobileNavigationDrawer?: 'main' | 'settings';
}) => { }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const setIsNavigationDrawerOpen = useSetRecoilState( const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerOpenState, isNavigationDrawerExpandedState,
); );
const setCurrentMobileNavigationDrawer = useSetRecoilState( const setCurrentMobileNavigationDrawer = useSetRecoilState(
currentMobileNavigationDrawerState, currentMobileNavigationDrawerState,
@ -32,13 +33,13 @@ const MobileNavigationDrawerStateSetterEffect = ({
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;
setIsNavigationDrawerOpen(true); setIsNavigationDrawerExpanded(true);
setCurrentMobileNavigationDrawer(mobileNavigationDrawer); setCurrentMobileNavigationDrawer(mobileNavigationDrawer);
}, [ }, [
isMobile, isMobile,
mobileNavigationDrawer, mobileNavigationDrawer,
setCurrentMobileNavigationDrawer, setCurrentMobileNavigationDrawer,
setIsNavigationDrawerOpen, setIsNavigationDrawerExpanded,
]); ]);
return null; return null;

View File

@ -0,0 +1,15 @@
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useRecoilValue } from 'recoil';
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const useIsSettingsDrawer = () => {
const isMobile = useIsMobile();
const isSettingsPage = useIsSettingsPage();
const currentMobileNavigationDrawer = useRecoilValue(
currentMobileNavigationDrawerState,
);
return isMobile
? currentMobileNavigationDrawer === 'settings'
: isSettingsPage;
};

View File

@ -1,18 +1,18 @@
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
import { View } from '@/views/types/View'; import { View } from '@/views/types/View';
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
const ORDERED_STANDARD_OBJECTS = [ const ORDERED_STANDARD_OBJECTS = [
'person', 'person',
'company', 'company',
@ -38,107 +38,105 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const currentPath = useLocation().pathname; const currentPath = useLocation().pathname;
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
// TODO: refactor this by splitting into separate components const renderObjectMetadataItems = () => {
return [
...objectMetadataItems
.filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => {
const indexA = ORDERED_STANDARD_OBJECTS.indexOf(
objectMetadataItemA.nameSingular,
);
const indexB = ORDERED_STANDARD_OBJECTS.indexOf(
objectMetadataItemB.nameSingular,
);
if (indexA === -1 || indexB === -1) {
return objectMetadataItemA.nameSingular.localeCompare(
objectMetadataItemB.nameSingular,
);
}
return indexA - indexB;
}),
...objectMetadataItems
.filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => {
return new Date(objectMetadataItemA.createdAt) <
new Date(objectMetadataItemB.createdAt)
? 1
: -1;
}),
].map((objectMetadataItem) => {
const objectMetadataViews = getObjectMetadataItemViews(
objectMetadataItem.id,
views,
);
const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId(
objectMetadataItem.id,
);
const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id;
const navigationPath = `/objects/${objectMetadataItem.namePlural}${
viewId ? `?view=${viewId}` : ''
}`;
const isActive =
currentPath === `/objects/${objectMetadataItem.namePlural}`;
const shouldSubItemsBeDisplayed =
isActive && objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) =>
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
);
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
(view) => viewId === view.id,
);
const subItemArrayLength = sortedObjectMetadataViews.length;
return (
<NavigationDrawerItemsCollapsedContainer
isGroup={shouldSubItemsBeDisplayed}
>
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={navigationPath}
Icon={getIcon(objectMetadataItem.icon)}
active={isActive}
/>
{shouldSubItemsBeDisplayed &&
sortedObjectMetadataViews.map((view, index) => (
<NavigationDrawerSubItem
label={view.name}
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
active={viewId === view.id}
subItemState={getNavigationSubItemState({
index,
arrayLength: subItemArrayLength,
selectedIndex: selectedSubItemIndex,
})}
Icon={getIcon(view.icon)}
key={view.id}
/>
))}
</NavigationDrawerItemsCollapsedContainer>
);
});
};
return ( return (
objectMetadataItems.length > 0 && ( objectMetadataItems.length > 0 && (
<NavigationDrawerSection> <NavigationDrawerSection>
<NavigationDrawerSectionTitle <NavigationDrawerAnimatedCollapseWrapper>
label={sectionTitle} <NavigationDrawerSectionTitle
onClick={() => toggleNavigationSection()} label={sectionTitle}
/> onClick={() => toggleNavigationSection()}
/>
{isNavigationSectionOpen && </NavigationDrawerAnimatedCollapseWrapper>
[ {isNavigationSectionOpen && renderObjectMetadataItems()}
...objectMetadataItems
.filter((item) =>
ORDERED_STANDARD_OBJECTS.includes(item.nameSingular),
)
.sort((objectMetadataItemA, objectMetadataItemB) => {
const indexA = ORDERED_STANDARD_OBJECTS.indexOf(
objectMetadataItemA.nameSingular,
);
const indexB = ORDERED_STANDARD_OBJECTS.indexOf(
objectMetadataItemB.nameSingular,
);
if (indexA === -1 || indexB === -1) {
return objectMetadataItemA.nameSingular.localeCompare(
objectMetadataItemB.nameSingular,
);
}
return indexA - indexB;
}),
...objectMetadataItems
.filter(
(item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular),
)
.sort((objectMetadataItemA, objectMetadataItemB) => {
return new Date(objectMetadataItemA.createdAt) <
new Date(objectMetadataItemB.createdAt)
? 1
: -1;
}),
].map((objectMetadataItem) => {
const objectMetadataViews = getObjectMetadataItemViews(
objectMetadataItem.id,
views,
);
const lastVisitedViewId =
getLastVisitedViewIdFromObjectMetadataItemId(
objectMetadataItem.id,
);
const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id;
const navigationPath = `/objects/${objectMetadataItem.namePlural}${
viewId ? `?view=${viewId}` : ''
}`;
const shouldSubItemsBeDisplayed =
currentPath === `/objects/${objectMetadataItem.namePlural}` &&
objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) =>
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
);
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
(view) => viewId === view.id,
);
const subItemArrayLength = sortedObjectMetadataViews.length;
return (
<div key={objectMetadataItem.id}>
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={navigationPath}
Icon={getIcon(objectMetadataItem.icon)}
active={
currentPath === `/objects/${objectMetadataItem.namePlural}`
}
/>
{shouldSubItemsBeDisplayed &&
sortedObjectMetadataViews.map((view, index) => (
<NavigationDrawerSubItem
label={view.name}
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
active={viewId === view.id}
subItemState={getNavigationSubItemState({
index,
arrayLength: subItemArrayLength,
selectedIndex: selectedSubItemIndex,
})}
Icon={getIcon(view.icon)}
key={view.id}
/>
))}
</div>
);
})}
</NavigationDrawerSection> </NavigationDrawerSection>
) )
); );

View File

@ -8,7 +8,7 @@ import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings'; import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize'; import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize';
import { css, Global, useTheme } from '@emotion/react'; import { css, Global, useTheme } from '@emotion/react';
@ -84,7 +84,7 @@ export const DefaultLayout = () => {
isSettingsPage && !isMobile isSettingsPage && !isMobile
? (windowsWidth - ? (windowsWidth -
(OBJECT_SETTINGS_WIDTH + (OBJECT_SETTINGS_WIDTH +
DESKTOP_NAV_DRAWER_WIDTHS.menu + NAV_DRAWER_WIDTHS.menu.desktop.expanded +
64)) / 64)) /
2 2
: 0, : 0,

View File

@ -13,7 +13,8 @@ import {
import { IconButton } from '@/ui/input/button/components/IconButton'; import { IconButton } from '@/ui/input/button/components/IconButton';
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton'; import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const PAGE_BAR_MIN_HEIGHT = 40; export const PAGE_BAR_MIN_HEIGHT = 40;
@ -108,12 +109,14 @@ export const PageHeader = ({
}: PageHeaderProps) => { }: PageHeaderProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const theme = useTheme(); const theme = useTheme();
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState); const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
return ( return (
<StyledTopBarContainer width={width}> <StyledTopBarContainer width={width}>
<StyledLeftContainer> <StyledLeftContainer>
{!isMobile && !isNavigationDrawerOpen && ( {!isMobile && !isNavigationDrawerExpanded && (
<StyledTopBarButtonContainer> <StyledTopBarButtonContainer>
<NavigationDrawerCollapseButton direction="right" /> <NavigationDrawerCollapseButton direction="right" />
</StyledTopBarButtonContainer> </StyledTopBarButtonContainer>

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconChevronDown } from 'twenty-ui'; import { IconChevronDown } from 'twenty-ui';
@ -15,6 +15,7 @@ import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/c
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
const StyledLogo = styled.div<{ logo: string }>` const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo}); background: url(${({ logo }) => logo});
@ -37,8 +38,6 @@ const StyledContainer = styled.div`
padding: calc(${({ theme }) => theme.spacing(1)} - 1px); padding: calc(${({ theme }) => theme.spacing(1)} - 1px);
width: 100%; width: 100%;
gap: ${({ theme }) => theme.spacing(1)};
&:hover { &:hover {
background-color: ${({ theme }) => theme.background.transparent.lighter}; background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
@ -48,6 +47,7 @@ const StyledContainer = styled.div`
const StyledLabel = styled.div` const StyledLabel = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
margin: 0 ${({ theme }) => theme.spacing(1)};
`; `;
const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
@ -95,11 +95,15 @@ export const MultiWorkspaceDropdownButton = ({
) ?? '' ) ?? ''
} }
/> />
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel> <NavigationDrawerAnimatedCollapseWrapper>
<StyledIconChevronDown <StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
size={theme.icon.size.md} </NavigationDrawerAnimatedCollapseWrapper>
stroke={theme.icon.stroke.sm} <NavigationDrawerAnimatedCollapseWrapper>
/> <StyledIconChevronDown
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledContainer> </StyledContainer>
} }
dropdownComponents={ dropdownComponents={

View File

@ -1,49 +1,45 @@
import { css, 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 { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MOBILE_VIEWPORT } from 'twenty-ui'; import { MOBILE_VIEWPORT } from 'twenty-ui';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { DESKTOP_NAV_DRAWER_WIDTHS } from '../constants/DesktopNavDrawerWidths'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { isNavigationDrawerExpandedState } from '../../states/isNavigationDrawerExpanded';
import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton';
import { NavigationDrawerHeader } from './NavigationDrawerHeader'; import { NavigationDrawerHeader } from './NavigationDrawerHeader';
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
export type NavigationDrawerProps = { export type NavigationDrawerProps = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
footer?: ReactNode; footer?: ReactNode;
isSubMenu?: boolean;
logo?: string; logo?: string;
title?: string; title?: string;
}; };
const StyledAnimatedContainer = styled(motion.div)` const StyledAnimatedContainer = styled(motion.div)``;
display: flex;
justify-content: end;
`;
const StyledContainer = styled.div<{ isSubMenu?: boolean }>` const StyledContainer = styled.div<{
isSettings?: boolean;
isMobile?: boolean;
}>`
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px;
gap: ${({ theme }) => theme.spacing(3)}; gap: ${({ theme }) => theme.spacing(3)};
height: 100%; height: 100%;
min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px; padding: ${({ theme, isSettings, isMobile }) =>
padding: ${({ theme }) => theme.spacing(3, 2, 4)}; isSettings
? isMobile
${({ isSubMenu, theme }) => ? theme.spacing(3, 8)
isSubMenu : theme.spacing(3, 8, 4, 0)
? css` : theme.spacing(3, 2, 4)};
padding-left: ${theme.spacing(0)};
padding-right: ${theme.spacing(8)};
`
: ''}
@media (max-width: ${MOBILE_VIEWPORT}px) { @media (max-width: ${MOBILE_VIEWPORT}px) {
width: 100%; width: 100%;
@ -61,15 +57,16 @@ export const NavigationDrawer = ({
children, children,
className, className,
footer, footer,
isSubMenu,
logo, logo,
title, title,
}: NavigationDrawerProps) => { }: NavigationDrawerProps) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isSettingsDrawer = useIsSettingsDrawer();
const theme = useTheme(); const theme = useTheme();
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState); const isNavigationDrawerExpanded = useRecoilValue(
const isSettingsPage = useIsSettingsPage(); isNavigationDrawerExpandedState,
);
const handleHover = () => { const handleHover = () => {
setIsHovered(true); setIsHovered(true);
@ -79,30 +76,35 @@ export const NavigationDrawer = ({
setIsHovered(false); setIsHovered(false);
}; };
const desktopWidth = !isNavigationDrawerOpen const desktopWidth = isNavigationDrawerExpanded
? 12 ? NAV_DRAWER_WIDTHS.menu.desktop.expanded
: DESKTOP_NAV_DRAWER_WIDTHS.menu; : NAV_DRAWER_WIDTHS.menu.desktop.collapsed;
const mobileWidth = isNavigationDrawerOpen ? '100%' : 0; const mobileWidth = isNavigationDrawerExpanded
? NAV_DRAWER_WIDTHS.menu.mobile.expanded
: NAV_DRAWER_WIDTHS.menu.mobile.collapsed;
const navigationDrawerAnimate = {
width: isMobile ? mobileWidth : desktopWidth,
opacity: isNavigationDrawerExpanded || !isSettingsDrawer ? 1 : 0,
};
return ( return (
<StyledAnimatedContainer <StyledAnimatedContainer
className={className} className={className}
initial={false} initial={false}
animate={{ animate={navigationDrawerAnimate}
width: isMobile ? mobileWidth : desktopWidth,
opacity: isNavigationDrawerOpen || isSettingsPage ? 1 : 0,
}}
transition={{ transition={{
duration: theme.animation.duration.normal, duration: theme.animation.duration.normal,
}} }}
> >
<StyledContainer <StyledContainer
isSubMenu={isSubMenu} isSettings={isSettingsDrawer}
isMobile={isMobile}
onMouseEnter={handleHover} onMouseEnter={handleHover}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
{isSubMenu && title ? ( {isSettingsDrawer && title ? (
!isMobile && <NavigationDrawerBackButton title={title} /> !isMobile && <NavigationDrawerBackButton title={title} />
) : ( ) : (
<NavigationDrawerHeader <NavigationDrawerHeader

View File

@ -0,0 +1,51 @@
import styled from '@emotion/styled';
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useTheme } from '@emotion/react';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
const StyledAnimatedContainer = styled(motion.div)``;
export const NavigationDrawerAnimatedCollapseWrapper = ({
children,
}: {
children: React.ReactNode;
}) => {
const theme = useTheme();
const isSettingsPage = useIsSettingsPage();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
if (isSettingsPage) {
return children;
}
const animate: AnimationControls | TargetAndTransition =
isNavigationDrawerExpanded
? {
opacity: 1,
width: 'auto',
height: 'auto',
pointerEvents: 'auto',
}
: {
opacity: 0,
width: 0,
height: 0,
pointerEvents: 'none',
};
return (
<StyledAnimatedContainer
initial={false}
animate={animate}
transition={{
duration: theme.animation.duration.normal,
}}
>
{children}
</StyledAnimatedContainer>
);
};

View File

@ -1,9 +1,11 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconX } from 'twenty-ui'; import { IconX } from 'twenty-ui';
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
type NavigationDrawerBackButtonProps = { type NavigationDrawerBackButtonProps = {
@ -43,9 +45,22 @@ export const NavigationDrawerBackButton = ({
const theme = useTheme(); const theme = useTheme();
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState); const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerExpandedState,
);
const navigationDrawerExpandedMemorized = useRecoilValue(
navigationDrawerExpandedMemorizedState,
);
return ( return (
<StyledContainer> <StyledContainer>
<UndecoratedLink to={navigationMemorizedUrl} replace> <UndecoratedLink
to={navigationMemorizedUrl}
replace
onClick={() =>
setIsNavigationDrawerExpanded(navigationDrawerExpandedMemorized)
}
>
<StyledIconAndButtonContainer> <StyledIconAndButtonContainer>
<IconX <IconX
size={theme.icon.size.md} size={theme.icon.size.md}

View File

@ -1,3 +1,5 @@
import { IconButton } from '@/ui/input/button/components/IconButton';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { import {
@ -5,9 +7,6 @@ import {
IconLayoutSidebarRightCollapse, IconLayoutSidebarRightCollapse,
} from 'twenty-ui'; } from 'twenty-ui';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
const StyledCollapseButton = styled.div` const StyledCollapseButton = styled.div`
align-items: center; align-items: center;
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
@ -33,15 +32,17 @@ export const NavigationDrawerCollapseButton = ({
className, className,
direction = 'left', direction = 'left',
}: NavigationDrawerCollapseButtonProps) => { }: NavigationDrawerCollapseButtonProps) => {
const setIsNavigationDrawerOpen = useSetRecoilState( const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerOpenState, isNavigationDrawerExpandedState,
); );
return ( return (
<StyledCollapseButton <StyledCollapseButton
className={className} className={className}
onClick={() => onClick={() =>
setIsNavigationDrawerOpen((previousIsOpen) => !previousIsOpen) setIsNavigationDrawerExpanded(
(previousIsExpanded) => !previousIsExpanded,
)
} }
> >
<IconButton <IconButton

View File

@ -7,17 +7,20 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
const StyledContainer = styled.div<{ isMultiWorkspace: boolean }>` const StyledContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
gap: ${({ theme, isMultiWorkspace }) =>
!isMultiWorkspace ? theme.spacing(2) : null};
height: ${({ theme }) => theme.spacing(8)}; height: ${({ theme }) => theme.spacing(8)};
user-select: none; user-select: none;
`; `;
const StyledSingleWorkspaceContainer = styled(StyledContainer)`
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledLogo = styled.div<{ logo: string }>` const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo}); background: url(${({ logo }) => logo});
@ -57,21 +60,25 @@ export const NavigationDrawerHeader = ({
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const workspaces = useRecoilValue(workspacesState); const workspaces = useRecoilValue(workspacesState);
const isMultiWorkspace = workspaces !== null && workspaces.length > 1; const isMultiWorkspace = workspaces !== null && workspaces.length > 1;
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
return ( return (
<StyledContainer isMultiWorkspace={isMultiWorkspace}> <StyledContainer>
{isMultiWorkspace ? ( {isMultiWorkspace ? (
<MultiWorkspaceDropdownButton workspaces={workspaces} /> <MultiWorkspaceDropdownButton workspaces={workspaces} />
) : ( ) : (
<> <StyledSingleWorkspaceContainer>
<StyledLogo <StyledLogo
logo={isNonEmptyString(logo) ? logo : DEFAULT_WORKSPACE_LOGO} logo={isNonEmptyString(logo) ? logo : DEFAULT_WORKSPACE_LOGO}
/> />
<StyledName>{name}</StyledName> <NavigationDrawerAnimatedCollapseWrapper>
</> <StyledName>{name}</StyledName>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledSingleWorkspaceContainer>
)} )}
{!isMobile && isNavigationDrawerExpanded && (
{!isMobile && (
<StyledNavigationDrawerCollapseButton <StyledNavigationDrawerCollapseButton
direction="left" direction="left"
show={showCollapseButton} show={showCollapseButton}

View File

@ -1,13 +1,14 @@
import { NavigationDrawerItemBreadcrumb } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb'; import { NavigationDrawerItemBreadcrumb } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb';
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState'; import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import isPropValid from '@emotion/is-prop-valid'; import isPropValid from '@emotion/is-prop-valid';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { import {
IconComponent, IconComponent,
MOBILE_VIEWPORT, MOBILE_VIEWPORT,
@ -15,6 +16,8 @@ import {
TablerIconsProps, TablerIconsProps,
} from 'twenty-ui'; } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
const DEFAULT_INDENTATION_LEVEL = 1; const DEFAULT_INDENTATION_LEVEL = 1;
@ -38,7 +41,7 @@ export type NavigationDrawerItemProps = {
type StyledItemProps = Pick< type StyledItemProps = Pick<
NavigationDrawerItemProps, NavigationDrawerItemProps,
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to' 'active' | 'danger' | 'indentationLevel' | 'soon' | 'to'
>; > & { isNavigationDrawerExpanded: boolean };
const StyledItem = styled('div', { const StyledItem = styled('div', {
shouldForwardProp: (prop) => shouldForwardProp: (prop) =>
@ -65,9 +68,8 @@ const StyledItem = styled('div', {
}}; }};
cursor: ${(props) => (props.soon ? 'default' : 'pointer')}; cursor: ${(props) => (props.soon ? 'default' : 'pointer')};
display: flex; display: flex;
font-family: 'Inter'; font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.md}; font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(1)}; padding-bottom: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(1)}; padding-left: ${({ theme }) => theme.spacing(1)};
@ -78,7 +80,12 @@ const StyledItem = styled('div', {
indentationLevel === 2 ? '2px' : '0'}; indentationLevel === 2 ? '2px' : '0'};
pointer-events: ${(props) => (props.soon ? 'none' : 'auto')}; pointer-events: ${(props) => (props.soon ? 'none' : 'auto')};
width: 100%;
width: ${(props) =>
!props.isNavigationDrawerExpanded
? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px`
: '100%'};
:hover { :hover {
background: ${({ theme }) => theme.background.transparent.light}; background: ${({ theme }) => theme.background.transparent.light};
color: ${(props) => color: ${(props) =>
@ -96,9 +103,14 @@ const StyledItem = styled('div', {
} }
`; `;
const StyledItemElementsContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledItemLabel = styled.div` const StyledItemLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
`; `;
@ -111,7 +123,6 @@ const StyledItemCount = styled.div`
display: flex; display: flex;
font-size: ${({ theme }) => theme.font.size.xs}; font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold}; font-weight: ${({ theme }) => theme.font.weight.semiBold};
height: 16px; height: 16px;
justify-content: center; justify-content: center;
margin-left: auto; margin-left: auto;
@ -151,16 +162,15 @@ export const NavigationDrawerItem = ({
}: NavigationDrawerItemProps) => { }: NavigationDrawerItemProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isSettingsPage = useIsSettingsPage();
const navigate = useNavigate(); const navigate = useNavigate();
const setIsNavigationDrawerOpen = useSetRecoilState( const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
isNavigationDrawerOpenState, useRecoilState(isNavigationDrawerExpandedState);
);
const showBreadcrumb = indentationLevel === 2; const showBreadcrumb = indentationLevel === 2;
const handleItemClick = () => { const handleItemClick = () => {
if (isMobile) { if (isMobile) {
setIsNavigationDrawerOpen(false); setIsNavigationDrawerExpanded(false);
} }
if (isDefined(onClick)) { if (isDefined(onClick)) {
@ -185,25 +195,51 @@ export const NavigationDrawerItem = ({
as={to ? Link : 'div'} as={to ? Link : 'div'}
to={to ? to : undefined} to={to ? to : undefined}
indentationLevel={indentationLevel} indentationLevel={indentationLevel}
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
> >
{showBreadcrumb && ( {showBreadcrumb && (
<NavigationDrawerItemBreadcrumb state={subItemState} /> <NavigationDrawerAnimatedCollapseWrapper>
)} <NavigationDrawerItemBreadcrumb state={subItemState} />
{Icon && ( </NavigationDrawerAnimatedCollapseWrapper>
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
/>
)}
<StyledItemLabel>{label}</StyledItemLabel>
{soon && <Pill label="Soon" />}
{!!count && <StyledItemCount>{count}</StyledItemCount>}
{keyboard && (
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard}
</StyledKeyBoardShortcut>
)} )}
<StyledItemElementsContainer>
{Icon && (
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={
showBreadcrumb && !isSettingsPage && !isNavigationDrawerExpanded
? theme.font.color.light
: 'currentColor'
}
/>
)}
<NavigationDrawerAnimatedCollapseWrapper>
<StyledItemLabel>{label}</StyledItemLabel>
</NavigationDrawerAnimatedCollapseWrapper>
{soon && (
<NavigationDrawerAnimatedCollapseWrapper>
<Pill label="Soon" />
</NavigationDrawerAnimatedCollapseWrapper>
)}
{!!count && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledItemCount>{count}</StyledItemCount>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{keyboard && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard}
</StyledKeyBoardShortcut>
</NavigationDrawerAnimatedCollapseWrapper>
)}
</StyledItemElementsContainer>
</StyledItem> </StyledItem>
</StyledNavigationDrawerItemContainer> </StyledNavigationDrawerItemContainer>
); );

View File

@ -6,9 +6,10 @@ export type NavigationDrawerItemBreadcrumbProps = {
}; };
const StyledNavigationDrawerItemBreadcrumbContainer = styled.div` const StyledNavigationDrawerItemBreadcrumbContainer = styled.div`
margin-left: 7.5px;
height: 28px; height: 28px;
margin-left: 7.5px;
margin-right: ${({ theme }) => theme.spacing(2)};
width: 9px; width: 9px;
`; `;

View File

@ -0,0 +1,52 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
import { useTheme } from '@emotion/react';
const StyledAnimationGroupContainer = styled(motion.div)``;
type NavigationDrawerItemsCollapsedContainerProps = {
isGroup?: boolean;
children: ReactNode;
};
export const NavigationDrawerItemsCollapsedContainer = ({
isGroup = false,
children,
}: NavigationDrawerItemsCollapsedContainerProps) => {
const theme = useTheme();
const isSettingsPage = useIsSettingsPage();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const isExpanded = isNavigationDrawerExpanded || isSettingsPage;
let animate: AnimationControls | TargetAndTransition = {
width: 'auto',
backgroundColor: 'transparent',
border: 'none',
};
if (!isExpanded) {
animate = { width: 24 };
if (isGroup) {
animate = {
width: 24,
backgroundColor: theme.background.transparent.lighter,
border: `1px solid ${theme.background.transparent.lighter}`,
borderRadius: theme.border.radius.sm,
};
}
}
return (
<StyledAnimationGroupContainer
initial={false}
animate={animate}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledAnimationGroupContainer>
);
};

View File

@ -1,8 +1,10 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader'; import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -37,9 +39,22 @@ export const NavigationDrawerSectionTitle = ({
}: NavigationDrawerSectionTitleProps) => { }: NavigationDrawerSectionTitleProps) => {
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const loading = useIsPrefetchLoading(); const loading = useIsPrefetchLoading();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const isSettingsPage = useIsSettingsPage();
if (loading && isDefined(currentUser)) { if (loading && isDefined(currentUser)) {
return <NavigationDrawerSectionTitleSkeletonLoader />; return <NavigationDrawerSectionTitleSkeletonLoader />;
} }
return <StyledTitle onClick={onClick}>{label}</StyledTitle>; return (
<StyledTitle
onClick={
isNavigationDrawerExpanded || isSettingsPage ? onClick : undefined
}
>
{label}
</StyledTitle>
);
}; };

View File

@ -53,6 +53,11 @@ export const Default: Story = {
Icon={IconBell} Icon={IconBell}
soon={true} soon={true}
/> />
<NavigationDrawerItem
label="Search"
Icon={IconSearch}
keyboard={['⌘', 'K']}
/>
<NavigationDrawerItem <NavigationDrawerItem
label="Settings" label="Settings"
to="/settings/profile" to="/settings/profile"
@ -84,9 +89,8 @@ export const Default: Story = {
}, },
}; };
export const Submenu: Story = { export const Settings: Story = {
args: { args: {
isSubMenu: true,
title: 'Settings', title: 'Settings',
children: ( children: (
<> <>

View File

@ -1,3 +0,0 @@
export const DESKTOP_NAV_DRAWER_WIDTHS = {
menu: 220,
};

View File

@ -0,0 +1,12 @@
export const NAV_DRAWER_WIDTHS = {
menu: {
mobile: {
collapsed: 0,
expanded: '100%',
},
desktop: {
collapsed: 40,
expanded: 220,
},
},
};

View File

@ -3,7 +3,7 @@ import { MOBILE_VIEWPORT } from 'twenty-ui';
const isMobile = window.innerWidth <= MOBILE_VIEWPORT; const isMobile = window.innerWidth <= MOBILE_VIEWPORT;
export const isNavigationDrawerOpenState = atom({ export const isNavigationDrawerExpandedState = atom({
key: 'isNavigationDrawerOpen', key: 'isNavigationDrawerExpanded',
default: !isMobile, default: !isMobile,
}); });

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { MOBILE_VIEWPORT } from 'twenty-ui';
const isMobile = window.innerWidth <= MOBILE_VIEWPORT;
export const navigationDrawerExpandedMemorizedState = atom({
key: 'navigationDrawerExpandedMemorized',
default: !isMobile,
});