diff --git a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx index c3bfeeb6a..2a11720b5 100644 --- a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { Avatar } from 'twenty-ui'; import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader'; @@ -8,6 +9,7 @@ import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableLi 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 { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { useFavorites } from '../hooks/useFavorites'; @@ -36,6 +38,10 @@ export const Favorites = () => { const { favorites, handleReorderFavorite } = useFavorites(); const loading = useIsPrefetchLoading(); + const { toggleNavigationSection, isNavigationSectionOpenState } = + useNavigationSection('Favorites'); + const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); + if (loading) { return ; } @@ -44,48 +50,53 @@ export const Favorites = () => { return ( - - - {favorites.map((favorite, index) => { - const { - id, - labelIdentifier, - avatarUrl, - avatarType, - link, - recordId, - } = favorite; - - return ( - ( - - )} - to={link} - /> - } - /> - ); - })} - - } + toggleNavigationSection()} /> + {isNavigationSectionOpen && ( + + {favorites.map((favorite, index) => { + const { + id, + labelIdentifier, + avatarUrl, + avatarType, + link, + recordId, + } = favorite; + + return ( + ( + + )} + to={link} + /> + } + /> + ); + })} + + } + /> + )} ); }; diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index a002010e8..43d7b3da3 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -9,7 +9,6 @@ import { Favorites } from '@/favorites/components/Favorites'; import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems'; 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 { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -56,10 +55,8 @@ export const MainNavigationDrawerItems = () => { - - - - + + ); }; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index cb28ad6d3..d546a1a4b 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -1,4 +1,5 @@ import { useLocation } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { useIcons } from 'twenty-ui'; import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader'; @@ -7,11 +8,21 @@ import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; 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 { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { View } from '@/views/types/View'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; -export const ObjectMetadataNavItems = () => { +export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { + const { toggleNavigationSection, isNavigationSectionOpenState } = + useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); + const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( + (item) => (isRemote ? item.isRemote : !item.isRemote), + ); const { getIcon } = useIcons(); const currentPath = useLocation().pathname; @@ -23,55 +34,69 @@ export const ObjectMetadataNavItems = () => { } return ( - <> - {[ - ...activeObjectMetadataItems - .filter((item) => - ['person', 'company', 'opportunity'].includes(item.nameSingular), - ) - .sort((objectMetadataItemA, objectMetadataItemB) => { - const order = ['person', 'company', 'opportunity']; - const indexA = order.indexOf(objectMetadataItemA.nameSingular); - const indexB = order.indexOf(objectMetadataItemB.nameSingular); - if (indexA === -1 || indexB === -1) { - return objectMetadataItemA.nameSingular.localeCompare( - objectMetadataItemB.nameSingular, - ); - } - return indexA - indexB; - }), - ...activeObjectMetadataItems - .filter( - (item) => - !['person', 'company', 'opportunity'].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 viewId = objectMetadataViews[0]?.id; + filteredActiveObjectMetadataItems.length > 0 && ( + + toggleNavigationSection()} + /> - const navigationPath = `/objects/${objectMetadataItem.namePlural}${ - viewId ? `?view=${viewId}` : '' - }`; + {isNavigationSectionOpen && + [ + ...filteredActiveObjectMetadataItems + .filter((item) => + ['person', 'company', 'opportunity'].includes( + item.nameSingular, + ), + ) + .sort((objectMetadataItemA, objectMetadataItemB) => { + const order = ['person', 'company', 'opportunity']; + const indexA = order.indexOf(objectMetadataItemA.nameSingular); + const indexB = order.indexOf(objectMetadataItemB.nameSingular); + if (indexA === -1 || indexB === -1) { + return objectMetadataItemA.nameSingular.localeCompare( + objectMetadataItemB.nameSingular, + ); + } + return indexA - indexB; + }), + ...filteredActiveObjectMetadataItems + .filter( + (item) => + !['person', 'company', 'opportunity'].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 viewId = objectMetadataViews[0]?.id; - return ( - - ); - })} - + const navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; + + return ( + + ); + })} + + ) ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx index 39052b657..3d3f69387 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx @@ -2,21 +2,34 @@ import styled from '@emotion/styled'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type NavigationDrawerSectionTitleProps = { + onClick?: () => void; label: string; }; -const StyledTitle = styled.div` +const StyledTitle = styled.div<{ onClick?: () => void }>` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.sm}; color: ${({ theme }) => theme.font.color.light}; display: flex; font-size: ${({ theme }) => theme.font.size.xs}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; + height: ${({ theme }) => theme.spacing(4)}; padding: ${({ theme }) => theme.spacing(1)}; - padding-top: 0; + + ${({ onClick, theme }) => + !isUndefinedOrNull(onClick) + ? `&:hover { + cursor: pointer; + background-color:${theme.background.transparent.light}; + }` + : ''} `; export const NavigationDrawerSectionTitle = ({ + onClick, label, }: NavigationDrawerSectionTitleProps) => { const loading = useIsPrefetchLoading(); @@ -24,5 +37,5 @@ export const NavigationDrawerSectionTitle = ({ if (loading) { return ; } - return {label}; + return {label}; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useNavigationSection.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useNavigationSection.ts new file mode 100644 index 000000000..b497319a0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useNavigationSection.ts @@ -0,0 +1,60 @@ +import { useRecoilCallback } from 'recoil'; + +import { isNavigationSectionOpenComponentState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenComponentState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; + +export const useNavigationSection = (scopeId: string) => { + const closeNavigationSection = useRecoilCallback( + ({ set }) => + () => { + set( + isNavigationSectionOpenComponentState({ + scopeId, + }), + false, + ); + }, + [scopeId], + ); + + const openNavigationSection = useRecoilCallback( + ({ set }) => + () => { + set( + isNavigationSectionOpenComponentState({ + scopeId, + }), + true, + ); + }, + [scopeId], + ); + + const toggleNavigationSection = useRecoilCallback( + ({ snapshot }) => + () => { + const isNavigationSectionOpen = snapshot + .getLoadable(isNavigationSectionOpenComponentState({ scopeId })) + .getValue(); + + if (isNavigationSectionOpen) { + closeNavigationSection(); + } else { + openNavigationSection(); + } + }, + [closeNavigationSection, openNavigationSection, scopeId], + ); + + const isNavigationSectionOpenState = extractComponentState( + isNavigationSectionOpenComponentState, + scopeId, + ); + + return { + isNavigationSectionOpenState, + closeNavigationSection, + openNavigationSection, + toggleNavigationSection, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isNavigationSectionOpenComponentState.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isNavigationSectionOpenComponentState.ts new file mode 100644 index 000000000..cd0fec7a0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isNavigationSectionOpenComponentState.ts @@ -0,0 +1,9 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { localStorageEffect } from '~/utils/recoil-effects'; + +export const isNavigationSectionOpenComponentState = + createComponentState({ + key: 'isNavigationSectionOpenComponentState', + defaultValue: true, + effects: [localStorageEffect()], + }); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts index 2e2cd8ada..b72932d86 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentState.ts @@ -1,16 +1,19 @@ -import { atomFamily } from 'recoil'; +import { AtomEffect, atomFamily } from 'recoil'; import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/ComponentStateKey'; export const createComponentState = ({ key, defaultValue, + effects, }: { key: string; defaultValue: ValueType; + effects?: AtomEffect[]; }) => { return atomFamily({ key, default: defaultValue, + effects: effects, }); }; diff --git a/packages/twenty-front/src/utils/recoil-effects.ts b/packages/twenty-front/src/utils/recoil-effects.ts index d7c0261b9..7f20e24c5 100644 --- a/packages/twenty-front/src/utils/recoil-effects.ts +++ b/packages/twenty-front/src/utils/recoil-effects.ts @@ -5,17 +5,17 @@ import { cookieStorage } from '~/utils/cookie-storage'; import { isDefined } from './isDefined'; export const localStorageEffect = - (key: string): AtomEffect => - ({ setSelf, onSet }) => { - const savedValue = localStorage.getItem(key); + (key?: string): AtomEffect => + ({ setSelf, onSet, node }) => { + const savedValue = localStorage.getItem(key ?? node.key); if (savedValue != null) { setSelf(JSON.parse(savedValue)); } onSet((newValue, _, isReset) => { isReset - ? localStorage.removeItem(key) - : localStorage.setItem(key, JSON.stringify(newValue)); + ? localStorage.removeItem(key ?? node.key) + : localStorage.setItem(key ?? node.key, JSON.stringify(newValue)); }); };