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));
});
};