mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-25 20:00:34 +03:00
feat - Compact sidebar (#7414)
This commit is contained in:
parent
c0610419c2
commit
a9deede9ba
@ -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={{
|
||||
|
@ -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;
|
||||
|
@ -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,54 +59,65 @@ export const CurrentWorkspaceMemberFavorites = () => {
|
||||
)
|
||||
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 (
|
||||
<StyledContainer>
|
||||
<NavigationDrawerSectionTitle
|
||||
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>
|
||||
<NavigationDrawerSectionTitle
|
||||
label="Favorites"
|
||||
onClick={() => toggleNavigationSection()}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
|
||||
{isNavigationSectionOpen && (
|
||||
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
|
||||
{draggableListContent}
|
||||
</NavigationDrawerItemsCollapsedContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
@ -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,107 +38,105 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const currentPath = useLocation().pathname;
|
||||
|
||||
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 (
|
||||
objectMetadataItems.length > 0 && (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={sectionTitle}
|
||||
onClick={() => toggleNavigationSection()}
|
||||
/>
|
||||
|
||||
{isNavigationSectionOpen &&
|
||||
[
|
||||
...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>
|
||||
);
|
||||
})}
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={sectionTitle}
|
||||
onClick={() => toggleNavigationSection()}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && renderObjectMetadataItems()}
|
||||
</NavigationDrawerSection>
|
||||
)
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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 = ({
|
||||
) ?? ''
|
||||
}
|
||||
/>
|
||||
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
|
||||
<StyledIconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledIconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
</StyledContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
<StyledName>{name}</StyledName>
|
||||
</>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledName>{name}</StyledName>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
</StyledSingleWorkspaceContainer>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
{!isMobile && isNavigationDrawerExpanded && (
|
||||
<StyledNavigationDrawerCollapseButton
|
||||
direction="left"
|
||||
show={showCollapseButton}
|
||||
|
@ -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 && (
|
||||
<NavigationDrawerItemBreadcrumb state={subItemState} />
|
||||
)}
|
||||
{Icon && (
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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: (
|
||||
<>
|
||||
|
@ -1,3 +0,0 @@
|
||||
export const DESKTOP_NAV_DRAWER_WIDTHS = {
|
||||
menu: 220,
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
export const NAV_DRAWER_WIDTHS = {
|
||||
menu: {
|
||||
mobile: {
|
||||
collapsed: 0,
|
||||
expanded: '100%',
|
||||
},
|
||||
desktop: {
|
||||
collapsed: 40,
|
||||
expanded: 220,
|
||||
},
|
||||
},
|
||||
};
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
Loading…
Reference in New Issue
Block a user