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 { 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 { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader';
@ -47,14 +47,14 @@ const StyledSkeletonTitleContainer = styled.div`
export const LeftPanelSkeletonLoader = () => {
const isMobile = useIsMobile();
const mobileWidth = isMobile ? 0 : '100%';
const desktopWidth = !mobileWidth ? 12 : DESKTOP_NAV_DRAWER_WIDTHS.menu;
return (
<StyledAnimatedContainer
initial={false}
animate={{
width: isMobile ? mobileWidth : desktopWidth,
width: isMobile
? NAV_DRAWER_WIDTHS.menu.mobile.collapsed
: NAV_DRAWER_WIDTHS.menu.desktop.expanded,
opacity: isMobile ? 0 : 1,
}}
transition={{

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
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 { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
@ -12,7 +12,7 @@ const StyledContainer = styled.div`
flex-direction: row;
gap: 12px;
height: 100dvh;
min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px;
min-width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px;
width: 100%;
padding: 12px 8px 12px 8px;
overflow: hidden;

View File

@ -13,6 +13,8 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
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)`
width: 100%;
@ -39,7 +41,6 @@ export const CurrentWorkspaceMemberFavorites = () => {
const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading();
const { toggleNavigationSection, isNavigationSectionOpenState } =
useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
@ -58,13 +59,9 @@ export const CurrentWorkspaceMemberFavorites = () => {
)
return <></>;
return (
<StyledContainer>
<NavigationDrawerSectionTitle
label="Favorites"
onClick={() => toggleNavigationSection()}
/>
{isNavigationSectionOpen && (
const isGroup = currentWorkspaceMemberFavorites.length > 1;
const draggableListContent = (
<DraggableList
onDragEnd={handleReorderFavorite}
draggableItems={
@ -106,6 +103,21 @@ export const CurrentWorkspaceMemberFavorites = () => {
</>
}
/>
);
return (
<StyledContainer>
<NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerSectionTitle
label="Favorites"
onClick={() => toggleNavigationSection()}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
{draggableListContent}
</NavigationDrawerItemsCollapsedContainer>
)}
</StyledContainer>
);

View File

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

View File

@ -1,5 +1,5 @@
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { IconSearch, IconSettings } from 'twenty-ui';
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 { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -23,6 +25,11 @@ export const MainNavigationDrawerItems = () => {
const isWorkspaceFavoriteEnabled = useIsFeatureEnabled(
'IS_WORKSPACE_FAVORITE_ENABLED',
);
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilState(
navigationDrawerExpandedMemorizedState,
);
return (
<>
@ -38,6 +45,8 @@ export const MainNavigationDrawerItems = () => {
label="Settings"
to={'/settings/profile'}
onClick={() => {
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
setIsNavigationDrawerExpanded(true);
setNavigationMemorizedUrl(location.pathname + location.search);
}}
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 { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
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 { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState';
@ -15,13 +13,12 @@ export const MobileNavigationBar = () => {
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
const { closeCommandMenu, openCommandMenu } = useCommandMenu();
const isSettingsPage = useIsSettingsPage();
const [isNavigationDrawerOpen, setIsNavigationDrawerOpen] = useRecoilState(
isNavigationDrawerOpenState,
);
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
useRecoilState(currentMobileNavigationDrawerState);
const activeItemName = isNavigationDrawerOpen
const activeItemName = isNavigationDrawerExpanded
? currentMobileNavigationDrawer
: isCommandMenuOpened
? 'search'
@ -39,7 +36,7 @@ export const MobileNavigationBar = () => {
Icon: IconList,
onClick: () => {
closeCommandMenu();
setIsNavigationDrawerOpen(
setIsNavigationDrawerExpanded(
(previousIsOpen) => activeItemName !== 'main' || !previousIsOpen,
);
setCurrentMobileNavigationDrawer('main');
@ -52,7 +49,7 @@ export const MobileNavigationBar = () => {
if (!isCommandMenuOpened) {
openCommandMenu();
}
setIsNavigationDrawerOpen(false);
setIsNavigationDrawerExpanded(false);
},
},
{
@ -60,7 +57,7 @@ export const MobileNavigationBar = () => {
Icon: IconSettings,
onClick: () => {
closeCommandMenu();
setIsNavigationDrawerOpen(
setIsNavigationDrawerExpanded(
(previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen,
);
setCurrentMobileNavigationDrawer('settings');

View File

@ -1,16 +1,17 @@
import { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
import { AppPath } from '@/types/AppPath';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import {
AppNavigationDrawer,
AppNavigationDrawerProps,
@ -22,8 +23,8 @@ const MobileNavigationDrawerStateSetterEffect = ({
mobileNavigationDrawer?: 'main' | 'settings';
}) => {
const isMobile = useIsMobile();
const setIsNavigationDrawerOpen = useSetRecoilState(
isNavigationDrawerOpenState,
const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerExpandedState,
);
const setCurrentMobileNavigationDrawer = useSetRecoilState(
currentMobileNavigationDrawerState,
@ -32,13 +33,13 @@ const MobileNavigationDrawerStateSetterEffect = ({
useEffect(() => {
if (!isMobile) return;
setIsNavigationDrawerOpen(true);
setIsNavigationDrawerExpanded(true);
setCurrentMobileNavigationDrawer(mobileNavigationDrawer);
}, [
isMobile,
mobileNavigationDrawer,
setCurrentMobileNavigationDrawer,
setIsNavigationDrawerOpen,
setIsNavigationDrawerExpanded,
]);
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
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 { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
import { View } from '@/views/types/View';
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 = [
'person',
'company',
@ -38,24 +38,12 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
const { getIcon } = useIcons();
const currentPath = useLocation().pathname;
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
// TODO: refactor this by splitting into separate components
return (
objectMetadataItems.length > 0 && (
<NavigationDrawerSection>
<NavigationDrawerSectionTitle
label={sectionTitle}
onClick={() => toggleNavigationSection()}
/>
{isNavigationSectionOpen &&
[
const renderObjectMetadataItems = () => {
return [
...objectMetadataItems
.filter((item) =>
ORDERED_STANDARD_OBJECTS.includes(item.nameSingular),
)
.filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => {
const indexA = ORDERED_STANDARD_OBJECTS.indexOf(
objectMetadataItemA.nameSingular,
@ -71,9 +59,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
return indexA - indexB;
}),
...objectMetadataItems
.filter(
(item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular),
)
.filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => {
return new Date(objectMetadataItemA.createdAt) <
new Date(objectMetadataItemB.createdAt)
@ -85,8 +71,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
objectMetadataItem.id,
views,
);
const lastVisitedViewId =
getLastVisitedViewIdFromObjectMetadataItemId(
const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId(
objectMetadataItem.id,
);
const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id;
@ -95,9 +80,10 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
viewId ? `?view=${viewId}` : ''
}`;
const isActive =
currentPath === `/objects/${objectMetadataItem.namePlural}`;
const shouldSubItemsBeDisplayed =
currentPath === `/objects/${objectMetadataItem.namePlural}` &&
objectMetadataViews.length > 1;
isActive && objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) =>
@ -111,15 +97,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
const subItemArrayLength = sortedObjectMetadataViews.length;
return (
<div key={objectMetadataItem.id}>
<NavigationDrawerItemsCollapsedContainer
isGroup={shouldSubItemsBeDisplayed}
>
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={navigationPath}
Icon={getIcon(objectMetadataItem.icon)}
active={
currentPath === `/objects/${objectMetadataItem.namePlural}`
}
active={isActive}
/>
{shouldSubItemsBeDisplayed &&
sortedObjectMetadataViews.map((view, index) => (
@ -136,9 +122,21 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
key={view.id}
/>
))}
</div>
</NavigationDrawerItemsCollapsedContainer>
);
})}
});
};
return (
objectMetadataItems.length > 0 && (
<NavigationDrawerSection>
<NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerSectionTitle
label={sectionTitle}
onClick={() => toggleNavigationSection()}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && renderObjectMetadataItems()}
</NavigationDrawerSection>
)
);

View File

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

View File

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

View File

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

View File

@ -1,49 +1,45 @@
import { css, useTheme } from '@emotion/react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { ReactNode, useState } from 'react';
import { useRecoilValue } from 'recoil';
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 { 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 { NavigationDrawerHeader } from './NavigationDrawerHeader';
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
export type NavigationDrawerProps = {
children: ReactNode;
className?: string;
footer?: ReactNode;
isSubMenu?: boolean;
logo?: string;
title?: string;
};
const StyledAnimatedContainer = styled(motion.div)`
display: flex;
justify-content: end;
`;
const StyledAnimatedContainer = styled(motion.div)``;
const StyledContainer = styled.div<{ isSubMenu?: boolean }>`
const StyledContainer = styled.div<{
isSettings?: boolean;
isMobile?: boolean;
}>`
box-sizing: border-box;
display: flex;
flex-direction: column;
width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px;
gap: ${({ theme }) => theme.spacing(3)};
height: 100%;
min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px;
padding: ${({ theme }) => theme.spacing(3, 2, 4)};
${({ isSubMenu, theme }) =>
isSubMenu
? css`
padding-left: ${theme.spacing(0)};
padding-right: ${theme.spacing(8)};
`
: ''}
padding: ${({ theme, isSettings, isMobile }) =>
isSettings
? isMobile
? theme.spacing(3, 8)
: theme.spacing(3, 8, 4, 0)
: theme.spacing(3, 2, 4)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 100%;
@ -61,15 +57,16 @@ export const NavigationDrawer = ({
children,
className,
footer,
isSubMenu,
logo,
title,
}: NavigationDrawerProps) => {
const [isHovered, setIsHovered] = useState(false);
const isMobile = useIsMobile();
const isSettingsDrawer = useIsSettingsDrawer();
const theme = useTheme();
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
const isSettingsPage = useIsSettingsPage();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const handleHover = () => {
setIsHovered(true);
@ -79,30 +76,35 @@ export const NavigationDrawer = ({
setIsHovered(false);
};
const desktopWidth = !isNavigationDrawerOpen
? 12
: DESKTOP_NAV_DRAWER_WIDTHS.menu;
const desktopWidth = isNavigationDrawerExpanded
? NAV_DRAWER_WIDTHS.menu.desktop.expanded
: 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 (
<StyledAnimatedContainer
className={className}
initial={false}
animate={{
width: isMobile ? mobileWidth : desktopWidth,
opacity: isNavigationDrawerOpen || isSettingsPage ? 1 : 0,
}}
animate={navigationDrawerAnimate}
transition={{
duration: theme.animation.duration.normal,
}}
>
<StyledContainer
isSubMenu={isSubMenu}
isSettings={isSettingsDrawer}
isMobile={isMobile}
onMouseEnter={handleHover}
onMouseLeave={handleMouseLeave}
>
{isSubMenu && title ? (
{isSettingsDrawer && title ? (
!isMobile && <NavigationDrawerBackButton title={title} />
) : (
<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 styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconX } from 'twenty-ui';
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';
type NavigationDrawerBackButtonProps = {
@ -43,9 +45,22 @@ export const NavigationDrawerBackButton = ({
const theme = useTheme();
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerExpandedState,
);
const navigationDrawerExpandedMemorized = useRecoilValue(
navigationDrawerExpandedMemorizedState,
);
return (
<StyledContainer>
<UndecoratedLink to={navigationMemorizedUrl} replace>
<UndecoratedLink
to={navigationMemorizedUrl}
replace
onClick={() =>
setIsNavigationDrawerExpanded(navigationDrawerExpandedMemorized)
}
>
<StyledIconAndButtonContainer>
<IconX
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 { useSetRecoilState } from 'recoil';
import {
@ -5,9 +7,6 @@ import {
IconLayoutSidebarRightCollapse,
} from 'twenty-ui';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
const StyledCollapseButton = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.md};
@ -33,15 +32,17 @@ export const NavigationDrawerCollapseButton = ({
className,
direction = 'left',
}: NavigationDrawerCollapseButtonProps) => {
const setIsNavigationDrawerOpen = useSetRecoilState(
isNavigationDrawerOpenState,
const setIsNavigationDrawerExpanded = useSetRecoilState(
isNavigationDrawerExpandedState,
);
return (
<StyledCollapseButton
className={className}
onClick={() =>
setIsNavigationDrawerOpen((previousIsOpen) => !previousIsOpen)
setIsNavigationDrawerExpanded(
(previousIsExpanded) => !previousIsExpanded,
)
}
>
<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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { isNonEmptyString } from '@sniptt/guards';
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;
display: flex;
gap: ${({ theme, isMultiWorkspace }) =>
!isMultiWorkspace ? theme.spacing(2) : null};
height: ${({ theme }) => theme.spacing(8)};
user-select: none;
`;
const StyledSingleWorkspaceContainer = styled(StyledContainer)`
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
@ -57,21 +60,25 @@ export const NavigationDrawerHeader = ({
const isMobile = useIsMobile();
const workspaces = useRecoilValue(workspacesState);
const isMultiWorkspace = workspaces !== null && workspaces.length > 1;
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
return (
<StyledContainer isMultiWorkspace={isMultiWorkspace}>
<StyledContainer>
{isMultiWorkspace ? (
<MultiWorkspaceDropdownButton workspaces={workspaces} />
) : (
<>
<StyledSingleWorkspaceContainer>
<StyledLogo
logo={isNonEmptyString(logo) ? logo : DEFAULT_WORKSPACE_LOGO}
/>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledName>{name}</StyledName>
</>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledSingleWorkspaceContainer>
)}
{!isMobile && (
{!isMobile && isNavigationDrawerExpanded && (
<StyledNavigationDrawerCollapseButton
direction="left"
show={showCollapseButton}

View File

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

View File

@ -6,9 +6,10 @@ export type NavigationDrawerItemBreadcrumbProps = {
};
const StyledNavigationDrawerItemBreadcrumbContainer = styled.div`
margin-left: 7.5px;
height: 28px;
margin-left: 7.5px;
margin-right: ${({ theme }) => theme.spacing(2)};
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 { currentUserState } from '@/auth/states/currentUserState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -37,9 +39,22 @@ export const NavigationDrawerSectionTitle = ({
}: NavigationDrawerSectionTitleProps) => {
const currentUser = useRecoilValue(currentUserState);
const loading = useIsPrefetchLoading();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const isSettingsPage = useIsSettingsPage();
if (loading && isDefined(currentUser)) {
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}
soon={true}
/>
<NavigationDrawerItem
label="Search"
Icon={IconSearch}
keyboard={['⌘', 'K']}
/>
<NavigationDrawerItem
label="Settings"
to="/settings/profile"
@ -84,9 +89,8 @@ export const Default: Story = {
},
};
export const Submenu: Story = {
export const Settings: Story = {
args: {
isSubMenu: true,
title: 'Settings',
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;
export const isNavigationDrawerOpenState = atom({
key: 'isNavigationDrawerOpen',
export const isNavigationDrawerExpandedState = atom({
key: 'isNavigationDrawerExpanded',
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,
});