feat: improve mobile display by tab bar and other changes (#2304)
* feat: improve mobile display by tab bar and other changes * fix: remove unused declaration in mobile navigation * fix: update desktop navbar stories title * fix: retrieve old titles for desktop-navbar stories * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: styles, manage active tabs * fix: update logic for tab bar menu icons * fix: remove Settings icon for mobile * fix: resolve comments in pl * feat: rework mobile navigation bar * Fix * Fixes --------- Co-authored-by: Thaïs Guigon <guigon.thais@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -2,8 +2,8 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import NavItem from '@/ui/navigation/navbar/components/NavItem';
|
||||
import NavTitle from '@/ui/navigation/navbar/components/NavTitle';
|
||||
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
|
||||
import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
|
||||
import { useFavorites } from '../hooks/useFavorites';
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { Favorites } from '@/favorites/components/Favorites';
|
||||
import { useIsTasksPage } from '@/navigation/hooks/useIsTasksPage';
|
||||
import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
|
||||
import {
|
||||
IconBell,
|
||||
IconCheckbox,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from '@/ui/display/icon/index';
|
||||
import MainNavbar from '@/ui/navigation/navigation-drawer/components/MainNavbar';
|
||||
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
|
||||
import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
|
||||
|
||||
import { WorkspaceNavItems } from './WorkspaceNavItems';
|
||||
|
||||
export const DesktopNavigationDrawer = () => {
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const isTasksPage = useIsTasksPage();
|
||||
const { currentUserDueTaskCount } = useCurrentUserTaskCount();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
return isSettingsPage ? (
|
||||
<SettingsNavbar />
|
||||
) : (
|
||||
<MainNavbar>
|
||||
<NavItem
|
||||
label="Search"
|
||||
Icon={IconSearch}
|
||||
onClick={toggleCommandMenu}
|
||||
keyboard={['⌘', 'K']}
|
||||
/>
|
||||
<NavItem label="Notifications" to="/inbox" Icon={IconBell} soon />
|
||||
<NavItem
|
||||
label="Settings"
|
||||
onClick={() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
navigate('/settings/profile');
|
||||
}}
|
||||
Icon={IconSettings}
|
||||
/>
|
||||
<NavItem
|
||||
label="Tasks"
|
||||
to="/tasks"
|
||||
active={isTasksPage}
|
||||
Icon={IconCheckbox}
|
||||
count={currentUserDueTaskCount}
|
||||
/>
|
||||
<Favorites />
|
||||
<WorkspaceNavItems />
|
||||
</MainNavbar>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,85 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconList,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from '@/ui/display/icon';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
|
||||
import { useIsSettingsPage } from '../hooks/useIsSettingsPage';
|
||||
import { useIsTasksPage } from '../hooks/useIsTasksPage';
|
||||
|
||||
type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
|
||||
|
||||
export const MobileNavigationBar = () => {
|
||||
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
|
||||
const { closeCommandMenu, toggleCommandMenu } = useCommandMenu();
|
||||
const isTasksPage = useIsTasksPage();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const navigate = useNavigate();
|
||||
const [navigationDrawer, setNavigationDrawer] = useRecoilState(
|
||||
navigationDrawerState,
|
||||
);
|
||||
|
||||
const initialActiveItemName: NavigationBarItemName = isCommandMenuOpened
|
||||
? 'search'
|
||||
: isTasksPage
|
||||
? 'tasks'
|
||||
: isSettingsPage
|
||||
? 'settings'
|
||||
: 'main';
|
||||
|
||||
const items: {
|
||||
name: NavigationBarItemName;
|
||||
Icon: IconComponent;
|
||||
onClick: () => void;
|
||||
}[] = [
|
||||
{
|
||||
name: 'main',
|
||||
Icon: IconList,
|
||||
onClick: () => {
|
||||
closeCommandMenu();
|
||||
setNavigationDrawer(navigationDrawer === 'main' ? '' : 'main');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
Icon: IconSearch,
|
||||
onClick: () => {
|
||||
setNavigationDrawer('');
|
||||
toggleCommandMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tasks',
|
||||
Icon: IconCheckbox,
|
||||
onClick: () => {
|
||||
closeCommandMenu();
|
||||
setNavigationDrawer('');
|
||||
navigate('/tasks');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
Icon: IconSettings,
|
||||
onClick: () => {
|
||||
closeCommandMenu();
|
||||
setNavigationDrawer(navigationDrawer === 'settings' ? '' : 'settings');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<NavigationBar
|
||||
activeItemName={navigationDrawer || initialActiveItemName}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { Favorites } from '@/favorites/components/Favorites';
|
||||
import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
|
||||
import MainNavbar from '@/ui/navigation/navigation-drawer/components/MainNavbar';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
|
||||
import { WorkspaceNavItems } from './WorkspaceNavItems';
|
||||
|
||||
export const MobileNavigationDrawer = () => {
|
||||
const [navigationDrawer] = useRecoilState(navigationDrawerState);
|
||||
|
||||
return navigationDrawer === 'settings' ? (
|
||||
<SettingsNavbar />
|
||||
) : (
|
||||
<MainNavbar>
|
||||
<Favorites />
|
||||
<WorkspaceNavItems />
|
||||
</MainNavbar>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems';
|
||||
import { IconTargetArrow } from '@/ui/display/icon/index';
|
||||
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
|
||||
import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
|
||||
|
||||
export const WorkspaceNavItems = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavTitle label="Workspace" />
|
||||
<ObjectMetadataNavItems />
|
||||
<NavItem
|
||||
label="Opportunities"
|
||||
to="/objects/opportunities"
|
||||
active={pathname === '/objects/opportunities'}
|
||||
Icon={IconTargetArrow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
|
||||
import { DesktopNavigationDrawer } from '../DesktopNavigationDrawer';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
const meta: Meta<typeof DesktopNavigationDrawer> = {
|
||||
title: 'Modules/Navigation/DesktopNavigationDrawer',
|
||||
component: DesktopNavigationDrawer,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DesktopNavigationDrawer>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ComponentWithRouterDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
|
||||
import { MobileNavigationDrawer } from '../MobileNavigationDrawer';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
const meta: Meta<typeof MobileNavigationDrawer> = {
|
||||
title: 'Modules/Navigation/MobileNavigationDrawer',
|
||||
component: MobileNavigationDrawer,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MobileNavigationDrawer>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ComponentWithRouterDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
};
|
||||
4
front/src/modules/navigation/hooks/useIsSettingsPage.ts
Normal file
4
front/src/modules/navigation/hooks/useIsSettingsPage.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const useIsSettingsPage = () =>
|
||||
useLocation().pathname.match(/\/settings\//g) !== null;
|
||||
3
front/src/modules/navigation/hooks/useIsTasksPage.ts
Normal file
3
front/src/modules/navigation/hooks/useIsTasksPage.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const useIsTasksPage = () => useLocation().pathname === '/tasks';
|
||||
@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { Icon123 } from '@/ui/input/constants/icons';
|
||||
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
|
||||
import NavItem from '@/ui/navigation/navbar/components/NavItem';
|
||||
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
|
||||
|
||||
export const ObjectMetadataNavItems = () => {
|
||||
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();
|
||||
|
||||
@ -89,12 +89,14 @@ export const RecordTableContainer = ({
|
||||
}}
|
||||
/>
|
||||
<RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} />
|
||||
<RecordTable
|
||||
recordTableId={recordTableId}
|
||||
viewBarId={viewBarId}
|
||||
updateRecordMutation={updateEntity}
|
||||
createRecord={createRecord}
|
||||
/>
|
||||
{
|
||||
<RecordTable
|
||||
recordTableId={recordTableId}
|
||||
viewBarId={viewBarId}
|
||||
updateRecordMutation={updateEntity}
|
||||
createRecord={createRecord}
|
||||
/>
|
||||
}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,9 +12,9 @@ import {
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from '@/ui/display/icon/index';
|
||||
import NavItem from '@/ui/navigation/navbar/components/NavItem';
|
||||
import NavTitle from '@/ui/navigation/navbar/components/NavTitle';
|
||||
import SubMenuNavbar from '@/ui/navigation/navbar/components/SubMenuNavbar';
|
||||
import NavItem from '@/ui/navigation/navigation-drawer/components/NavItem';
|
||||
import NavTitle from '@/ui/navigation/navigation-drawer/components/NavTitle';
|
||||
import SubMenuNavbar from '@/ui/navigation/navigation-drawer/components/SubMenuNavbar';
|
||||
|
||||
export const SettingsNavbar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IconArchiveOff, IconDotsVertical, IconTrash } from '@/ui/display/icon';
|
||||
import { IconArchiveOff, IconDotsVertical } from '@/ui/display/icon';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
@ -15,9 +15,7 @@ type SettingsObjectFieldDisabledActionDropdownProps = {
|
||||
};
|
||||
|
||||
export const SettingsObjectFieldDisabledActionDropdown = ({
|
||||
isCustomField,
|
||||
onActivate,
|
||||
onErase,
|
||||
scopeKey,
|
||||
}: SettingsObjectFieldDisabledActionDropdownProps) => {
|
||||
const dropdownScopeId = `${scopeKey}-settings-field-disabled-action-dropdown`;
|
||||
@ -29,10 +27,10 @@ export const SettingsObjectFieldDisabledActionDropdown = ({
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleErase = () => {
|
||||
onErase();
|
||||
closeDropdown();
|
||||
};
|
||||
// const handleErase = () => {
|
||||
// onErase();
|
||||
// closeDropdown();
|
||||
// };
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
@ -48,14 +46,14 @@ export const SettingsObjectFieldDisabledActionDropdown = ({
|
||||
LeftIcon={IconArchiveOff}
|
||||
onClick={handleActivate}
|
||||
/>
|
||||
{isCustomField && (
|
||||
{/* {isCustomField && (
|
||||
<MenuItem
|
||||
text="Erase"
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleErase}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IconDotsVertical, IconTrash } from '@/ui/display/icon';
|
||||
import { IconDotsVertical } from '@/ui/display/icon';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { IconArchiveOff } from '@/ui/input/constants/icons';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@ -16,9 +16,7 @@ type SettingsObjectDisabledMenuDropDownProps = {
|
||||
};
|
||||
|
||||
export const SettingsObjectDisabledMenuDropDown = ({
|
||||
isCustomObject,
|
||||
onActivate,
|
||||
onErase,
|
||||
scopeKey,
|
||||
}: SettingsObjectDisabledMenuDropDownProps) => {
|
||||
const dropdownScopeId = `${scopeKey}-settings-object-disabled-menu-dropdown`;
|
||||
@ -30,10 +28,10 @@ export const SettingsObjectDisabledMenuDropDown = ({
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleErase = () => {
|
||||
onErase();
|
||||
closeDropdown();
|
||||
};
|
||||
// const handleErase = () => {
|
||||
// onErase();
|
||||
// closeDropdown();
|
||||
// };
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
@ -49,14 +47,14 @@ export const SettingsObjectDisabledMenuDropDown = ({
|
||||
LeftIcon={IconArchiveOff}
|
||||
onClick={handleActivate}
|
||||
/>
|
||||
{isCustomObject && (
|
||||
{/* {isCustomObject && (
|
||||
<MenuItem
|
||||
text="Erase"
|
||||
LeftIcon={IconTrash}
|
||||
accent="danger"
|
||||
onClick={handleErase}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const useIsMenuNavbarDisplayed = () => {
|
||||
const currentPath = useLocation().pathname;
|
||||
return currentPath.match(/^\/companies(\/.*)?$/) !== null;
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const useIsSubMenuNavbarDisplayed = () => {
|
||||
const currentPath = useLocation().pathname;
|
||||
return currentPath.match(/\/settings\//g) !== null;
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { AuthModal } from '@/auth/components/Modal';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
@ -8,23 +8,22 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||
import { DesktopNavigationDrawer } from '@/navigation/components/DesktopNavigationDrawer';
|
||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||
import { MobileNavigationDrawer } from '@/navigation/components/MobileNavigationDrawer';
|
||||
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
|
||||
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { AppNavbar } from '~/AppNavbar';
|
||||
|
||||
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
|
||||
import { NavbarAnimatedContainer } from '@/ui/navigation/navigation-drawer/components/NavbarAnimatedContainer';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
scrollbar-color: ${({ theme }) => theme.border.color.medium};
|
||||
|
||||
scrollbar-width: 4px;
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
@ -41,43 +40,51 @@ const StyledLayout = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
const StyledPageContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 0 1 100%;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: ${() => (useRecoilValue(isNavbarOpenedState) ? '0' : '100%')};
|
||||
}
|
||||
`;
|
||||
|
||||
type DefaultLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isMobile = useIsMobile();
|
||||
return (
|
||||
<StyledLayout>
|
||||
<CommandMenu />
|
||||
<KeyboardShortcutMenu />
|
||||
<NavbarAnimatedContainer>
|
||||
<AppNavbar />
|
||||
</NavbarAnimatedContainer>
|
||||
<StyledMainContainer>
|
||||
{onboardingStatus && onboardingStatus !== OnboardingStatus.Completed ? (
|
||||
<>
|
||||
<SignInBackgroundMockPage />
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
<AuthModal>{children}</AuthModal>
|
||||
</LayoutGroup>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<AppErrorBoundary>{children}</AppErrorBoundary>
|
||||
)}
|
||||
</StyledMainContainer>
|
||||
<StyledPageContainer>
|
||||
<NavbarAnimatedContainer>
|
||||
{isMobile ? <MobileNavigationDrawer /> : <DesktopNavigationDrawer />}
|
||||
</NavbarAnimatedContainer>
|
||||
<StyledMainContainer>
|
||||
{onboardingStatus &&
|
||||
onboardingStatus !== OnboardingStatus.Completed ? (
|
||||
<>
|
||||
<SignInBackgroundMockPage />
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
<AuthModal>{children}</AuthModal>
|
||||
</LayoutGroup>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<AppErrorBoundary>{children}</AppErrorBoundary>
|
||||
)}
|
||||
</StyledMainContainer>
|
||||
</StyledPageContainer>
|
||||
{isMobile && <MobileNavigationBar />}
|
||||
</StyledLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,12 +1 @@
|
||||
import { PAGE_BAR_MIN_HEIGHT } from './PageHeader';
|
||||
import { RightDrawerContainer } from './RightDrawerContainer';
|
||||
|
||||
type PageBodyProps = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
};
|
||||
|
||||
export const PageBody = ({ children }: PageBodyProps) => (
|
||||
<RightDrawerContainer topMargin={PAGE_BAR_MIN_HEIGHT + 16 + 16}>
|
||||
{children}
|
||||
</RightDrawerContainer>
|
||||
);
|
||||
export { RightDrawerContainer as PageBody } from './RightDrawerContainer';
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type PageContainerProps = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const PageContainer = ({ children }: PageContainerProps) => (
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
);
|
||||
export { StyledContainer as PageContainer };
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ComponentProps, useCallback } from 'react';
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
@ -7,15 +7,12 @@ import { useRecoilValue } from 'recoil';
|
||||
import { IconChevronLeft } from '@/ui/display/icon/index';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
import {
|
||||
IconButton,
|
||||
IconButtonSize,
|
||||
} from '@/ui/input/button/components/IconButton';
|
||||
import NavCollapseButton from '@/ui/navigation/navbar/components/NavCollapseButton';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import NavCollapseButton from '@/ui/navigation/navigation-drawer/components/NavCollapseButton';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
|
||||
|
||||
export const PAGE_BAR_MIN_HEIGHT = 40;
|
||||
|
||||
const StyledTopBarContainer = styled.div`
|
||||
@ -31,13 +28,23 @@ const StyledTopBarContainer = styled.div`
|
||||
padding-left: 0;
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
z-index: 20;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLeftContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
@ -47,24 +54,15 @@ const StyledTitleContainer = styled.div`
|
||||
max-width: 50%;
|
||||
`;
|
||||
|
||||
const StyledTopBarButtonContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledBackIconButton = styled(IconButton)`
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledTopBarIconStyledTitleContainer = styled.div<{
|
||||
hideLeftPadding?: boolean;
|
||||
}>`
|
||||
const StyledTopBarIconStyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 0 100%;
|
||||
flex-direction: row;
|
||||
padding-left: ${({ theme, hideLeftPadding }) =>
|
||||
hideLeftPadding ? theme.spacing(2) : undefined};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPageActionContainer = styled.div`
|
||||
@ -72,11 +70,16 @@ const StyledPageActionContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTopBarButtonContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type PageHeaderProps = ComponentProps<'div'> & {
|
||||
title: string;
|
||||
hasBackButton?: boolean;
|
||||
Icon: IconComponent;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const PageHeader = ({
|
||||
@ -85,33 +88,28 @@ export const PageHeader = ({
|
||||
Icon,
|
||||
children,
|
||||
}: PageHeaderProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
const navigateBack = useCallback(() => navigate(-1), [navigate]);
|
||||
|
||||
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
|
||||
|
||||
const iconSize: IconButtonSize = useIsMobile() ? 'small' : 'medium';
|
||||
const theme = useTheme();
|
||||
const navigationDrawer = useRecoilValue(navigationDrawerState);
|
||||
|
||||
return (
|
||||
<StyledTopBarContainer>
|
||||
<StyledLeftContainer>
|
||||
{!isNavbarOpened && (
|
||||
{navigationDrawer === '' && (
|
||||
<StyledTopBarButtonContainer>
|
||||
<NavCollapseButton direction="right" />
|
||||
</StyledTopBarButtonContainer>
|
||||
)}
|
||||
{hasBackButton && (
|
||||
<StyledTopBarButtonContainer>
|
||||
<StyledBackIconButton
|
||||
Icon={IconChevronLeft}
|
||||
size={iconSize}
|
||||
onClick={navigateBack}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</StyledTopBarButtonContainer>
|
||||
<StyledBackIconButton
|
||||
Icon={IconChevronLeft}
|
||||
size={isMobile ? 'small' : 'medium'}
|
||||
onClick={() => navigate(-1)}
|
||||
variant="tertiary"
|
||||
/>
|
||||
)}
|
||||
<StyledTopBarIconStyledTitleContainer hideLeftPadding={!hasBackButton}>
|
||||
<StyledTopBarIconStyledTitleContainer>
|
||||
{Icon && <Icon size={theme.icon.size.md} />}
|
||||
<StyledTitleContainer data-testid="top-bar-title">
|
||||
<OverflowingTextWithTooltip text={title} />
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
|
||||
import { PagePanel } from './PagePanel';
|
||||
|
||||
type RightDrawerContainerProps = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
topMargin?: number;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StyledMainContainer = styled.div<{ topMargin: number }>`
|
||||
const StyledMainContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: calc(100% - ${(props) => props.topMargin}px);
|
||||
|
||||
height: 100%;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(3)});
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type LeftContainerProps = {
|
||||
@ -35,9 +40,8 @@ const StyledLeftContainer = styled.div<LeftContainerProps>`
|
||||
|
||||
export const RightDrawerContainer = ({
|
||||
children,
|
||||
topMargin,
|
||||
}: RightDrawerContainerProps) => (
|
||||
<StyledMainContainer topMargin={topMargin ?? 0}>
|
||||
<StyledMainContainer>
|
||||
<StyledLeftContainer>
|
||||
<PagePanel>{children}</PagePanel>
|
||||
</StyledLeftContainer>
|
||||
|
||||
@ -30,7 +30,7 @@ export const SubMenuTopBarContainer = ({
|
||||
return (
|
||||
<StyledContainer isMobile={isMobile}>
|
||||
{isMobile && <PageHeader title={title} Icon={Icon} />}
|
||||
<RightDrawerContainer topMargin={16}>{children}</RightDrawerContainer>
|
||||
<RightDrawerContainer>{children}</RightDrawerContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { leftNavbarWidth } from '../../../navigation/navbar/constants';
|
||||
import { useRightDrawer } from '../hooks/useRightDrawer';
|
||||
import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState';
|
||||
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
||||
@ -70,15 +69,9 @@ export const RightDrawer = () => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const rightDrawerWidthExpanded = `calc(100% - ${
|
||||
leftNavbarWidth.desktop
|
||||
} - ${theme.spacing(2)})`;
|
||||
|
||||
const rightDrawerWidth = isRightDrawerOpen
|
||||
? isMobile
|
||||
? isMobile || isRightDrawerExpanded
|
||||
? '100%'
|
||||
: isRightDrawerExpanded
|
||||
? rightDrawerWidthExpanded
|
||||
: theme.rightDrawerWidth
|
||||
: '0';
|
||||
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
|
||||
const isMobile = window.innerWidth <= MOBILE_VIEWPORT;
|
||||
|
||||
export const isNavbarOpenedState = atom({
|
||||
key: 'ui/isNavbarOpenedState',
|
||||
default: !isMobile,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isNavbarSwitchingSizeState = atom({
|
||||
key: 'ui/isNavbarSwitchingSizeState',
|
||||
default: true,
|
||||
});
|
||||
@ -1,63 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useIsSubMenuNavbarDisplayed } from '@/ui/layout/hooks/useIsSubMenuNavbarDisplayed';
|
||||
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState';
|
||||
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { leftNavbarWidth, leftSubMenuNavbarWidth } from '../constants';
|
||||
|
||||
const StyledNavbarContainer = styled(motion.div)`
|
||||
align-items: end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type NavbarAnimatedContainerProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const NavbarAnimatedContainer = ({
|
||||
children,
|
||||
}: NavbarAnimatedContainerProps) => {
|
||||
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
|
||||
const [, setIsNavbarSwitchingSize] = useRecoilState(
|
||||
isNavbarSwitchingSizeState,
|
||||
);
|
||||
const isInSubMenu = useIsSubMenuNavbarDisplayed();
|
||||
const theme = useTheme();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const leftBarWidth = isInSubMenu
|
||||
? isMobile
|
||||
? leftSubMenuNavbarWidth.mobile
|
||||
: leftSubMenuNavbarWidth.desktop
|
||||
: isMobile
|
||||
? leftNavbarWidth.mobile
|
||||
: leftNavbarWidth.desktop;
|
||||
|
||||
return (
|
||||
<StyledNavbarContainer
|
||||
onAnimationComplete={() => {
|
||||
setIsNavbarSwitchingSize(false);
|
||||
}}
|
||||
initial={false}
|
||||
animate={{
|
||||
width: isNavbarOpened ? leftBarWidth : '0',
|
||||
opacity: isNavbarOpened ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledNavbarContainer>
|
||||
);
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
export const leftNavbarWidth = {
|
||||
mobile: 'calc(100% - 16px)',
|
||||
desktop: '220px',
|
||||
};
|
||||
|
||||
export const leftSubMenuNavbarWidth = {
|
||||
mobile: 'calc(100% - 16px)',
|
||||
desktop: '520px',
|
||||
};
|
||||
|
||||
export const githubLink = 'https://github.com/twentyhq/twenty';
|
||||
@ -0,0 +1,33 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
import { NavigationBarItem } from './NavigationBarItem';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
type NavigationBarProps = {
|
||||
activeItemName: string;
|
||||
items: { name: string; Icon: IconComponent; onClick: () => void }[];
|
||||
};
|
||||
|
||||
export const NavigationBar = ({
|
||||
activeItemName,
|
||||
items,
|
||||
}: NavigationBarProps) => (
|
||||
<StyledContainer>
|
||||
{items.map(({ Icon, name, onClick }) => (
|
||||
<NavigationBarItem
|
||||
key={name}
|
||||
Icon={Icon}
|
||||
isActive={activeItemName === name}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -0,0 +1,42 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
const StyledIconButton = styled.div<{ isActive?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ isActive, theme }) =>
|
||||
isActive ? theme.background.transparent.light : 'none'};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(10)};
|
||||
justify-content: center;
|
||||
transition: background-color ${({ theme }) => theme.animation.duration.fast}s
|
||||
ease;
|
||||
width: ${({ theme }) => theme.spacing(10)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
type NavigationBarItemProps = {
|
||||
Icon: IconComponent;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const NavigationBarItem = ({
|
||||
Icon,
|
||||
isActive,
|
||||
onClick,
|
||||
}: NavigationBarItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledIconButton isActive={isActive} onClick={onClick}>
|
||||
<Icon color={theme.color.gray50} size={theme.icon.size.lg} />
|
||||
</StyledIconButton>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
|
||||
import { NavigationBar } from '../NavigationBar';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import {
|
||||
IconList,
|
||||
IconSearch,
|
||||
IconCheckbox,
|
||||
IconSettings,
|
||||
} from '@/ui/display/icon';
|
||||
|
||||
const meta: Meta<typeof NavigationBar> = {
|
||||
title: 'UI/Navigation/NavigationBar/NavigationBar',
|
||||
component: NavigationBar,
|
||||
args: {
|
||||
activeItemName: 'main',
|
||||
items: [
|
||||
{ name: 'main', Icon: IconList, onClick: () => undefined },
|
||||
{ name: 'search', Icon: IconSearch, onClick: () => undefined },
|
||||
{ name: 'tasks', Icon: IconCheckbox, onClick: () => undefined },
|
||||
{ name: 'settings', Icon: IconSettings, onClick: () => undefined },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof NavigationBar>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
|
||||
};
|
||||
@ -10,11 +10,13 @@ type MainNavbarProps = {
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2.5)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconChevronLeft } from '@/ui/display/icon/index';
|
||||
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
|
||||
type NavBackButtonProps = {
|
||||
title: string;
|
||||
@ -32,24 +32,19 @@ const StyledContainer = styled.div`
|
||||
|
||||
const NavBackButton = ({ title }: NavBackButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [, setIsNavbarSwitchingSize] = useRecoilState(
|
||||
isNavbarSwitchingSizeState,
|
||||
);
|
||||
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer>
|
||||
<StyledIconAndButtonContainer
|
||||
onClick={() => {
|
||||
setIsNavbarSwitchingSize(true);
|
||||
navigate('/', { replace: true });
|
||||
}}
|
||||
>
|
||||
<IconChevronLeft />
|
||||
<span>{title}</span>
|
||||
</StyledIconAndButtonContainer>
|
||||
</StyledContainer>
|
||||
</>
|
||||
<StyledContainer>
|
||||
<StyledIconAndButtonContainer
|
||||
onClick={() => {
|
||||
navigate(navigationMemorizedUrl, { replace: true });
|
||||
}}
|
||||
>
|
||||
<IconChevronLeft />
|
||||
<span>{title}</span>
|
||||
</StyledIconAndButtonContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarRightCollapse,
|
||||
} from '@/ui/display/icon';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
|
||||
const StyledCollapseButton = styled(motion.div)`
|
||||
align-items: center;
|
||||
@ -41,8 +41,7 @@ const NavCollapseButton = ({
|
||||
direction = 'left',
|
||||
show = true,
|
||||
}: NavCollapseButtonProps) => {
|
||||
const [isNavbarOpened, setIsNavbarOpened] =
|
||||
useRecoilState(isNavbarOpenedState);
|
||||
const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
|
||||
|
||||
const iconSize = 'small';
|
||||
const theme = useTheme();
|
||||
@ -57,7 +56,11 @@ const NavCollapseButton = ({
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
onClick={() => setIsNavbarOpened(!isNavbarOpened)}
|
||||
onClick={() =>
|
||||
setNavigationDrawer((navigationDrawer) =>
|
||||
navigationDrawer === '' ? 'main' : '',
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
Icon={
|
||||
@ -1,15 +1,15 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { isNavbarOpenedState } from '../../../layout/states/isNavbarOpenedState';
|
||||
|
||||
type NavItemProps = {
|
||||
className?: string;
|
||||
label: string;
|
||||
to?: string;
|
||||
onClick?: () => void;
|
||||
@ -115,6 +115,7 @@ const StyledKeyBoardShortcut = styled.div`
|
||||
`;
|
||||
|
||||
const NavItem = ({
|
||||
className,
|
||||
label,
|
||||
Icon,
|
||||
to,
|
||||
@ -126,25 +127,26 @@ const NavItem = ({
|
||||
keyboard,
|
||||
}: NavItemProps) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [, setIsNavbarOpened] = useRecoilState(isNavbarOpenedState);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (isMobile) {
|
||||
setIsNavbarOpened(false);
|
||||
setNavigationDrawer('');
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (to) {
|
||||
navigate(to);
|
||||
return;
|
||||
}
|
||||
|
||||
if (to) navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledItem
|
||||
className={className}
|
||||
onClick={handleItemClick}
|
||||
active={active}
|
||||
aria-selected={active}
|
||||
@ -157,7 +159,7 @@ const NavItem = ({
|
||||
{!!count && <StyledItemCount>{count}</StyledItemCount>}
|
||||
{keyboard && (
|
||||
<StyledKeyBoardShortcut className="keyboard-shortcuts">
|
||||
{keyboard.map((key) => key)}
|
||||
{keyboard}
|
||||
</StyledKeyBoardShortcut>
|
||||
)}
|
||||
</StyledItem>
|
||||
@ -2,6 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||
|
||||
import NavCollapseButton from './NavCollapseButton';
|
||||
@ -53,7 +54,7 @@ const NavWorkspaceButton = ({
|
||||
showCollapseButton,
|
||||
}: NavWorkspaceButtonProps) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const DEFAULT_LOGO =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||
|
||||
@ -69,7 +70,9 @@ const NavWorkspaceButton = ({
|
||||
></StyledLogo>
|
||||
<StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName>
|
||||
</StyledLogoAndNameContainer>
|
||||
<NavCollapseButton direction="left" show={showCollapseButton} />
|
||||
{!isMobile && (
|
||||
<NavCollapseButton direction="left" show={showCollapseButton} />
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { desktopNavDrawerWidths } from '../constants';
|
||||
|
||||
const StyledNavbarContainer = styled(motion.div)`
|
||||
align-items: end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
type NavbarAnimatedContainerProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NavbarAnimatedContainer = ({
|
||||
children,
|
||||
}: NavbarAnimatedContainerProps) => {
|
||||
const navigationDrawer = useRecoilValue(navigationDrawerState);
|
||||
|
||||
const isInSubMenu = useIsSettingsPage();
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const desktopWidth =
|
||||
navigationDrawer === ''
|
||||
? 12
|
||||
: isInSubMenu
|
||||
? desktopNavDrawerWidths.submenu
|
||||
: desktopNavDrawerWidths.menu;
|
||||
|
||||
return (
|
||||
<StyledNavbarContainer
|
||||
initial={false}
|
||||
animate={{
|
||||
width: !isMobile ? desktopWidth : navigationDrawer ? '100%' : 0,
|
||||
opacity: navigationDrawer === '' ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledNavbarContainer>
|
||||
);
|
||||
};
|
||||
@ -1,17 +1,19 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconBrandGithub } from '@/ui/display/icon';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import packageJson from '../../../../../../package.json';
|
||||
import { githubLink, leftNavbarWidth } from '../constants';
|
||||
import { desktopNavDrawerWidths, githubLink } from '../constants';
|
||||
|
||||
import NavBackButton from './NavBackButton';
|
||||
import NavItemsContainer from './NavItemsContainer';
|
||||
|
||||
type SubMenuNavbarProps = {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
backButtonTitle: string;
|
||||
displayVersion?: boolean;
|
||||
};
|
||||
@ -25,10 +27,11 @@ const StyledVersionContainer = styled.div`
|
||||
|
||||
const StyledVersion = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledVersionLink = styled.a`
|
||||
@ -36,18 +39,25 @@ const StyledVersionLink = styled.a`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
padding-top: ${({ theme }) => theme.spacing(9)};
|
||||
width: ${() => (useIsMobile() ? '100%' : leftNavbarWidth.desktop)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(11)};
|
||||
width: ${desktopNavDrawerWidths.menu};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const SubMenuNavbar = ({
|
||||
@ -56,13 +66,14 @@ const SubMenuNavbar = ({
|
||||
displayVersion,
|
||||
}: SubMenuNavbarProps) => {
|
||||
const version = packageJson.version;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<div>
|
||||
<NavBackButton title={backButtonTitle} />
|
||||
{!isMobile && <NavBackButton title={backButtonTitle} />}
|
||||
<NavItemsContainer>{children}</NavItemsContainer>
|
||||
</div>
|
||||
{displayVersion && (
|
||||
@ -18,7 +18,7 @@ import NavItem from '../NavItem';
|
||||
import NavTitle from '../NavTitle';
|
||||
|
||||
const meta: Meta<typeof MainNavbar> = {
|
||||
title: 'UI/Navigation/Navbar/MainNavbar',
|
||||
title: 'UI/Navigation/NavigationDrawer/MainNavbar',
|
||||
component: MainNavbar,
|
||||
decorators: [SnackBarDecorator],
|
||||
};
|
||||
@ -5,7 +5,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import NavCollapseButton from '../NavCollapseButton';
|
||||
|
||||
const meta: Meta<typeof NavCollapseButton> = {
|
||||
title: 'UI/Navigation/Navbar/NavCollapseButton',
|
||||
title: 'UI/Navigation/NavigationDrawer/NavCollapseButton',
|
||||
component: NavCollapseButton,
|
||||
};
|
||||
|
||||
@ -9,8 +9,14 @@ import { CatalogStory } from '~/testing/types';
|
||||
import NavItem from '../NavItem';
|
||||
|
||||
const meta: Meta<typeof NavItem> = {
|
||||
title: 'UI/Navigation/Navbar/NavItem',
|
||||
title: 'UI/Navigation/NavigationDrawer/NavItem',
|
||||
component: NavItem,
|
||||
args: {
|
||||
label: 'Search',
|
||||
Icon: IconSearch,
|
||||
active: true,
|
||||
},
|
||||
argTypes: { Icon: { control: false } },
|
||||
};
|
||||
|
||||
const StyledNavItemContainer = styled.div`
|
||||
@ -28,19 +34,11 @@ const ComponentDecorator: Decorator = (Story) => (
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof NavItem>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Search',
|
||||
Icon: IconSearch,
|
||||
onClick: () => console.log('clicked'),
|
||||
active: true,
|
||||
},
|
||||
argTypes: { Icon: { control: false }, onClick: { control: false } },
|
||||
export const Default: Story = {
|
||||
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof NavItem> = {
|
||||
args: Default.args,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
CatalogDecorator,
|
||||
@ -75,21 +73,28 @@ export const Catalog: CatalogStory<Story, typeof NavItem> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Soon: Story = {
|
||||
export const WithSoonPill: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
active: false,
|
||||
soon: true,
|
||||
},
|
||||
argTypes: { Icon: { control: false }, onClick: { control: false } },
|
||||
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
|
||||
};
|
||||
|
||||
export const Count: Story = {
|
||||
export const WithCount: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
count: 3,
|
||||
},
|
||||
argTypes: { Icon: { control: false }, onClick: { control: false } },
|
||||
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
|
||||
};
|
||||
|
||||
export const WithKeyboardKeys: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
className: "hover",
|
||||
keyboard: ['⌘', 'K'],
|
||||
},
|
||||
parameters: {
|
||||
pseudo: { hover: [".hover"] },
|
||||
}
|
||||
};
|
||||
@ -14,7 +14,7 @@ import NavTitle from '../NavTitle';
|
||||
import SubMenuNavbar from '../SubMenuNavbar';
|
||||
|
||||
const meta: Meta<typeof SubMenuNavbar> = {
|
||||
title: 'UI/Navigation/Navbar/SubMenuNavbar',
|
||||
title: 'UI/Navigation/NavigationDrawer/SubMenuNavbar',
|
||||
component: SubMenuNavbar,
|
||||
};
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export const desktopNavDrawerWidths = {
|
||||
menu: '236px',
|
||||
submenu: '536px',
|
||||
};
|
||||
|
||||
export const githubLink = 'https://github.com/twentyhq/twenty';
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const navigationDrawerState = atom<'main' | 'settings' | ''>({
|
||||
key: 'ui/navigationDrawerState',
|
||||
default: 'main',
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const navigationMemorizedUrlState = atom<string>({
|
||||
key: 'navigationMemorizedUrlState',
|
||||
default: '/',
|
||||
});
|
||||
@ -6,16 +6,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable';
|
||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
||||
import {
|
||||
RecordTableRow,
|
||||
StyledRow,
|
||||
} from '@/ui/object/record-table/components/RecordTableRow';
|
||||
import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext';
|
||||
import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext';
|
||||
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { RowIdContext } from '../contexts/RowIdContext';
|
||||
import { RowIndexContext } from '../contexts/RowIndexContext';
|
||||
import { useRecordTable } from '../hooks/useRecordTable';
|
||||
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
import { RecordTableRow, StyledRow } from './RecordTableRow';
|
||||
|
||||
export const RecordTableBody = () => {
|
||||
const { ref: lastTableRowRef, inView: lastTableRowIsVisible } = useInView();
|
||||
|
||||
@ -41,6 +43,7 @@ export const RecordTableBody = () => {
|
||||
isFetchingRecordTableDataState,
|
||||
);
|
||||
|
||||
// Todo, move this to an effect to not trigger many re-renders
|
||||
const { fetchMoreRecords: fetchMoreObjects } = useObjectRecordTable();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user