Restore old favorite design (#18049)

Issue : With IS_NAVIGATION_MENU_ITEM_ENABLED:true +
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED:false, nav menu design is
changed after 1.18.0 release : views expansion removed, system object
displayed, position re-ordered

We prefer keeping the same "old" favorite behaviour and design state

- After 1.18.0 all workspaces have up-to-date navigation menu items
(migrated)
- IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED becomes the FF for nav menu
new design

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2026-02-18 21:35:17 +01:00
committed by GitHub
parent 6b3ef404b0
commit 4cb64c6aa5
24 changed files with 326 additions and 144 deletions
@@ -2,24 +2,16 @@ import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/hooks/useCreateNavigationMenuItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const AddToFavoritesSingleRecordAction = () => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const recordId = useSelectedRecordIdOrThrow();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { createFavorite } = useCreateFavorite();
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
@@ -28,11 +20,7 @@ export const AddToFavoritesSingleRecordAction = () => {
return;
}
if (isNavigationMenuItemEnabled) {
createNavigationMenuItem(selectedRecord, objectMetadataItem.nameSingular);
} else {
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
}
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
};
return <Action onClick={handleClick} />;
@@ -8,9 +8,7 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const DeleteSingleRecordAction = () => {
const { recordIndexId, objectMetadataItem } =
@@ -29,9 +27,7 @@ export const DeleteSingleRecordAction = () => {
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { navigationMenuItems, workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const { removeNavigationMenuItemsByTargetRecordIds } =
@@ -50,15 +46,13 @@ export const DeleteSingleRecordAction = () => {
deleteFavorite(foundFavorite.id);
}
if (isNavigationMenuItemEnabled) {
const foundNavigationMenuItem = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
].find((item) => item.targetRecordId === recordId);
const foundNavigationMenuItem = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
].find((item) => item.targetRecordId === recordId);
if (isDefined(foundNavigationMenuItem)) {
removeNavigationMenuItemsByTargetRecordIds([recordId]);
}
if (isDefined(foundNavigationMenuItem)) {
removeNavigationMenuItemsByTargetRecordIds([recordId]);
}
await deleteOneRecord(recordId);
@@ -5,9 +5,7 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const RemoveFromFavoritesSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
@@ -16,9 +14,6 @@ export const RemoveFromFavoritesSingleRecordAction = () => {
const { sortedFavorites: favorites } = useFavorites();
const { navigationMenuItems, workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { deleteFavorite } = useDeleteFavorite();
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
@@ -27,28 +22,21 @@ export const RemoveFromFavoritesSingleRecordAction = () => {
(favorite) => favorite.recordId === recordId,
);
const foundNavigationMenuItem = isNavigationMenuItemEnabled
? [...navigationMenuItems, ...workspaceNavigationMenuItems].find(
(item) =>
item.targetRecordId === recordId &&
item.targetObjectMetadataId === objectMetadataItem.id,
)
: undefined;
const foundNavigationMenuItem = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
].find(
(item) =>
item.targetRecordId === recordId &&
item.targetObjectMetadataId === objectMetadataItem.id,
);
const handleClick = () => {
if (isNavigationMenuItemEnabled) {
if (!isDefined(foundNavigationMenuItem)) {
return;
}
deleteNavigationMenuItem(foundNavigationMenuItem.id);
return;
}
if (!isDefined(foundFavorite)) {
if (!isDefined(foundNavigationMenuItem) || !isDefined(foundFavorite)) {
return;
}
deleteNavigationMenuItem(foundNavigationMenuItem.id);
deleteFavorite(foundFavorite.id);
};
@@ -29,8 +29,8 @@ export const useShouldActionBeRegisteredParams = ({
}): ShouldBeRegisteredFunctionParams => {
const { sortedFavorites: favorites } = useFavorites();
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
@@ -47,7 +47,7 @@ export const useShouldActionBeRegisteredParams = ({
return false;
}
if (isNavigationMenuItemEnabled && isDefined(objectMetadataItem)) {
if (isNavigationMenuItemEditingEnabled && isDefined(objectMetadataItem)) {
const foundNavigationMenuItem = navigationMenuItems?.find(
(item) =>
item.targetRecordId === recordId &&
@@ -62,7 +62,7 @@ export const useShouldActionBeRegisteredParams = ({
return !!foundFavorite;
}, [
recordId,
isNavigationMenuItemEnabled,
isNavigationMenuItemEditingEnabled,
objectMetadataItem,
navigationMenuItems,
favorites,
@@ -5,6 +5,7 @@ import { generateCreateOneRecordMutation } from '@/object-metadata/utils/generat
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { AvatarType } from 'twenty-ui/display';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
@@ -105,7 +106,8 @@ export const sortedFavorites = [
},
];
const favoriteObjectMetadataItem = getMockObjectMetadataItemOrThrow('favorite')
const favoriteObjectMetadataItem = getMockObjectMetadataItemOrThrow('favorite');
const personObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
const UPDATE_ONE_FAVORITE_MUTATION = generateUpdateOneRecordMutation(
{
@@ -240,6 +242,41 @@ export const mocks = [
},
})),
},
{
request: {
query: CreateNavigationMenuItemDocument,
variables: {
input: {
targetRecordId: favoriteTargetObjectId,
targetObjectMetadataId: personObjectMetadataItem.id,
userWorkspaceId: '1',
folderId: undefined,
position: 1,
},
},
},
result: jest.fn(() => ({
data: {
createNavigationMenuItem: {
__typename: 'NavigationMenuItem',
id: mockId,
userWorkspaceId: '1',
targetRecordId: favoriteTargetObjectId,
targetObjectMetadataId: personObjectMetadataItem.id,
viewId: null,
folderId: null,
name: null,
link: null,
icon: null,
position: 1,
applicationId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
},
})),
},
];
export const mockWorkspaceMember = {
@@ -23,6 +23,23 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
useFindManyRecords: () => ({ records: initialFavorites }),
}));
jest.mock(
'@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData',
() => ({
usePrefetchedNavigationMenuItemsData: () => ({
navigationMenuItems: [],
workspaceNavigationMenuItems: [],
currentWorkspaceMemberId: '1',
}),
}),
);
jest.mock('@/favorites/hooks/usePrefetchedFavoritesFoldersData', () => ({
usePrefetchedFavoritesFoldersData: () => ({
favoriteFolders: [],
}),
}));
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks,
});
@@ -1,15 +1,31 @@
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
export const useCreateFavorite = () => {
const { favorites, currentWorkspaceMemberId } = usePrefetchedFavoritesData();
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const createFavorite = (
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
refetchQueries: ['FindManyNavigationMenuItems'],
});
const createFavorite = async (
targetRecord: ObjectRecord,
targetObjectNameSingular: string,
favoriteFolderId?: string,
@@ -25,12 +41,78 @@ export const useCreateFavorite = () => {
0,
);
createOneFavorite({
await createOneFavorite({
[`${targetObjectNameSingular}Id`]: targetRecord.id,
position: maxPosition + 1,
forWorkspaceMemberId: currentWorkspaceMemberId,
favoriteFolderId,
});
let navigationMenuItemFolderId: string | undefined;
if (isDefined(favoriteFolderId)) {
const favoriteFolder = favoriteFolders.find(
(folder) => folder.id === favoriteFolderId,
);
if (isDefined(favoriteFolder)) {
const matchingNavigationMenuItemFolder = navigationMenuItems.find(
(item) =>
isNavigationMenuItemFolder(item) &&
item.name === favoriteFolder.name &&
item.userWorkspaceId === currentWorkspaceMemberId,
);
navigationMenuItemFolderId = matchingNavigationMenuItemFolder?.id;
}
}
const relevantNavigationMenuItems = navigationMenuItemFolderId
? navigationMenuItems.filter(
(item) => item.folderId === navigationMenuItemFolderId,
)
: navigationMenuItems.filter(
(item) =>
!item.folderId && item.userWorkspaceId === currentWorkspaceMemberId,
);
const maxNavigationMenuItemPosition = Math.max(
...relevantNavigationMenuItems.map((item) => item.position),
0,
);
const isView = targetObjectNameSingular === 'view';
if (isView) {
await createNavigationMenuItemMutation({
variables: {
input: {
viewId: targetRecord.id,
userWorkspaceId: currentWorkspaceMemberId,
folderId: navigationMenuItemFolderId,
position: maxNavigationMenuItemPosition + 1,
},
},
});
} else {
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObjectNameSingular,
);
if (isDefined(objectMetadataItem)) {
await createNavigationMenuItemMutation({
variables: {
input: {
targetRecordId: targetRecord.id,
targetObjectMetadataId: objectMetadataItem.id,
userWorkspaceId: currentWorkspaceMemberId,
folderId: navigationMenuItemFolderId,
position: maxNavigationMenuItemPosition + 1,
},
},
});
}
}
};
return { createFavorite };
@@ -1,5 +1,8 @@
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
@@ -10,6 +13,12 @@ export const useCreateFavoriteFolder = () => {
const { currentWorkspaceMemberId } = usePrefetchedFavoritesData();
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
refetchQueries: ['FindManyNavigationMenuItems'],
});
const createNewFavoriteFolder = async (name: string): Promise<void> => {
if (!name || !currentWorkspaceMemberId) {
@@ -25,6 +34,34 @@ export const useCreateFavoriteFolder = () => {
name,
position: maxPosition + 1,
});
const folderNavigationMenuItems = navigationMenuItems.filter(
(item) =>
isDefined(item.name) &&
!item.folderId &&
!item.targetRecordId &&
!item.targetObjectMetadataId &&
!item.viewId &&
item.userWorkspaceId === currentWorkspaceMemberId,
);
const maxNavigationMenuItemPosition = Math.max(
...folderNavigationMenuItems.map((item) => item.position),
0,
);
await createNavigationMenuItemMutation({
variables: {
input: {
name,
targetRecordId: null,
targetObjectMetadataId: null,
userWorkspaceId: currentWorkspaceMemberId,
folderId: null,
position: maxNavigationMenuItemPosition + 1,
},
},
});
};
return { createNewFavoriteFolder };
@@ -5,11 +5,11 @@ import { CurrentWorkspaceMemberNavigationMenuItemFolders } from '@/navigation-me
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
if (isNavigationMenuItemEnabled) {
if (isNavigationMenuItemEditingEnabled) {
return <CurrentWorkspaceMemberNavigationMenuItemFolders />;
}
@@ -20,11 +20,11 @@ export const NavigationMenuItemFolderContentDispatcherEffect = ({
favorites,
navigationMenuItems,
}: NavigationMenuItemFolderContentDispatcherEffectProps) => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
if (isNavigationMenuItemEnabled && isDefined(navigationMenuItems)) {
if (isNavigationMenuItemEditingEnabled && isDefined(navigationMenuItems)) {
return (
<NavigationMenuItemFolderContent
folderId={folderId}
@@ -5,11 +5,11 @@ import { WorkspaceNavigationMenuItems } from '@/navigation-menu-item/components/
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const WorkspaceNavigationMenuItemsDispatcher = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
if (isNavigationMenuItemEnabled) {
if (isNavigationMenuItemEditingEnabled) {
return <WorkspaceNavigationMenuItems />;
}
@@ -1,5 +1,5 @@
import { useRecoilValue } from 'recoil';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
@@ -31,8 +31,8 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
);
const { favoritesByFolder } = useFavoritesByFolder();
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const openedFavoriteFolder = favoritesByFolder.find(
@@ -43,7 +43,7 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
(f) => f.id === currentNavigationMenuItemFolderId,
);
const openedFolder = isNavigationMenuItemEnabled
const openedFolder = isNavigationMenuItemEditingEnabled
? openedNavigationMenuItemFolder
: openedFavoriteFolder;
@@ -59,7 +59,7 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
</NavigationDrawerFixedContent>
<NavigationDrawerScrollableContent>
{isNavigationMenuItemEnabled ? (
{isNavigationMenuItemEditingEnabled ? (
<StyledScrollableContent>
{openedFolder ? (
<NavigationMenuItemFolderContentDispatcherEffect
@@ -20,8 +20,8 @@ type NavbarDragProviderProps = {
};
export const NavbarDragProvider = ({ children }: NavbarDragProviderProps) => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const [isDragging, setIsDragging] = useState(false);
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
@@ -42,7 +42,7 @@ export const NavbarDragProvider = ({ children }: NavbarDragProviderProps) => {
setIsDragging(false);
setSourceDroppableId(null);
if (isNavigationMenuItemEnabled) {
if (isNavigationMenuItemEditingEnabled) {
const isWorkspaceDrop =
(result.source?.droppableId?.startsWith('workspace-') ?? false) &&
(result.destination?.droppableId?.startsWith('workspace-') ?? false);
@@ -35,8 +35,8 @@ type PageDragDropProviderProps = {
export const PageDragDropProvider = ({
children,
}: PageDragDropProviderProps) => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const [isDragging, setIsDragging] = useState(false);
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
@@ -136,7 +136,7 @@ export const PageDragDropProvider = ({
return;
}
if (isNavigationMenuItemEnabled) {
if (isNavigationMenuItemEditingEnabled) {
const isWorkspaceDrop =
isWorkspaceDroppableId(result.source?.droppableId) &&
isWorkspaceDroppableId(result.destination?.droppableId);
@@ -1,3 +1,5 @@
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
@@ -6,6 +8,11 @@ import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/l
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { ViewKey } from '@/views/types/ViewKey';
import { useTheme } from '@emotion/react';
import { useLocation } from 'react-router-dom';
@@ -13,6 +20,7 @@ import { useRecoilValue } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -45,6 +53,17 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
lastVisitedViewPerObjectMetadataItemState,
);
const views = useRecoilValue(
coreViewsFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const contextStoreCurrentViewId = useRecoilComponentValue(
contextStoreCurrentViewIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const lastVisitedViewId =
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
@@ -136,6 +155,60 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
? objectMetadataItem.labelSingular
: undefined;
const shouldSubItemsBeDisplayed =
!isNavigationMenuItemEditingEnabled && isActive && views.length > 1;
const sortedObjectMetadataViews = [...views].sort(
(viewA, viewB) => viewA.position - viewB.position,
);
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
(view) => contextStoreCurrentViewId === view.id,
);
const subItemArrayLength = sortedObjectMetadataViews.length;
if (!isNavigationMenuItemEditingEnabled) {
return (
<NavigationDrawerItemsCollapsableContainer
isGroup={shouldSubItemsBeDisplayed}
>
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={navigationPath}
Icon={getIcon(objectMetadataItem.icon)}
active={isActive}
/>
<AnimatedExpandableContainer
isExpanded={shouldSubItemsBeDisplayed}
dimension="height"
mode="fit-content"
containAnimation
>
{sortedObjectMetadataViews.map((view, index) => (
<NavigationDrawerSubItem
label={view.name}
to={getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
{ viewId: view.id },
)}
active={contextStoreCurrentViewId === view.id}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: subItemArrayLength,
selectedIndex: selectedSubItemIndex,
})}
Icon={getIcon(view.icon)}
key={view.id}
/>
))}
</AnimatedExpandableContainer>
</NavigationDrawerItemsCollapsableContainer>
);
}
return (
<NavigationDrawerItem
label={label}
@@ -29,8 +29,8 @@ export const NavigationDrawerOpenedSection = () => {
const { workspaceFavoritesObjectMetadataItems } = useWorkspaceFavorites();
const { workspaceNavigationMenuItemsObjectMetadataItems } =
useWorkspaceNavigationMenuItems();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const {
@@ -52,7 +52,7 @@ export const NavigationDrawerOpenedSection = () => {
return;
}
const workspaceItemsToExclude = isNavigationMenuItemEnabled
const workspaceItemsToExclude = isNavigationMenuItemEditingEnabled
? workspaceNavigationMenuItemsObjectMetadataItems
: workspaceFavoritesObjectMetadataItems;
@@ -1,4 +1,5 @@
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
@@ -15,12 +16,9 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { sleep } from '~/utils/sleep';
type useDeleteManyRecordProps = {
@@ -60,9 +58,7 @@ export const useDeleteManyRecords = ({
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { removeNavigationMenuItemsByTargetRecordIds } =
useRemoveNavigationMenuItemByTargetRecordId();
@@ -231,9 +227,7 @@ export const useDeleteManyRecords = ({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
if (isNavigationMenuItemEnabled) {
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDelete);
}
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDelete);
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
@@ -1,6 +1,8 @@
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -11,12 +13,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
@@ -55,9 +53,6 @@ export const useDestroyManyRecords = ({
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { removeNavigationMenuItemsByTargetRecordIds } =
useRemoveNavigationMenuItemByTargetRecordId();
@@ -152,9 +147,7 @@ export const useDestroyManyRecords = ({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
if (isNavigationMenuItemEnabled) {
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDestroy);
}
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDestroy);
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
@@ -1,4 +1,5 @@
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@@ -16,11 +17,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { sleep } from '~/utils/sleep';
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
@@ -64,9 +62,6 @@ export const useIncrementalDeleteManyRecords = <T>({
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { refetchAggregateQueries } = useRefetchAggregateQueries();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const { removeNavigationMenuItemsByTargetRecordIds } =
useRemoveNavigationMenuItemByTargetRecordId();
@@ -244,9 +239,7 @@ export const useIncrementalDeleteManyRecords = <T>({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
if (isNavigationMenuItemEnabled) {
removeNavigationMenuItemsByTargetRecordIds(allDeletedRecordIds);
}
removeNavigationMenuItemsByTargetRecordIds(allDeletedRecordIds);
dispatchObjectRecordOperationBrowserEvent({
objectMetadataItem,
@@ -23,8 +23,8 @@ import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const PrefetchRunFavoriteQueriesEffect = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const showAuthModal = useShowAuthModal();
const isSettingsPage = useIsSettingsPage();
@@ -64,7 +64,7 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
isNavigationMenuItemEnabled,
isNavigationMenuItemEditingEnabled,
});
const { records: favoriteFolders } = useFindManyRecords({
@@ -75,7 +75,7 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
isNavigationMenuItemEnabled,
isNavigationMenuItemEditingEnabled,
});
const setPrefetchFavoritesState = useRecoilCallback(
@@ -18,8 +18,8 @@ import {
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const PrefetchRunNavigationMenuItemQueriesEffect = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const showAuthModal = useShowAuthModal();
@@ -37,7 +37,7 @@ export const PrefetchRunNavigationMenuItemQueriesEffect = () => {
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
!isNavigationMenuItemEnabled,
!isNavigationMenuItemEditingEnabled,
fetchPolicy: 'cache-and-network',
});
@@ -1,6 +1,5 @@
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/hooks/useCreateNavigationMenuItem';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -24,8 +23,8 @@ import {
import { MenuItem } from 'twenty-ui/navigation';
import {
FeatureFlagKey,
ViewVisibility,
PermissionFlagType,
ViewVisibility,
} from '~/generated-metadata/graphql';
type ViewPickerOptionDropdownProps = {
@@ -63,10 +62,9 @@ export const ViewPickerOptionDropdown = ({
const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite();
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
const { navigationMenuItems, currentWorkspaceMemberId } =
usePrefetchedNavigationMenuItemsData();
@@ -75,7 +73,7 @@ export const ViewPickerOptionDropdown = ({
const canEditView =
hasViewsPermission || view.visibility === ViewVisibility.UNLISTED;
const isFavorite = isNavigationMenuItemEnabled
const isFavorite = isNavigationMenuItemEditingEnabled
? navigationMenuItems.some(
(item) =>
item.viewId === view.id &&
@@ -94,11 +92,7 @@ export const ViewPickerOptionDropdown = ({
const handleAddToFavorites = () => {
if (!isFavorite) {
if (isNavigationMenuItemEnabled) {
createNavigationMenuItem(view, 'view');
} else {
createFavorite(view, 'view');
}
createFavorite(view, 'view');
} else {
setViewPickerReferenceViewId(view.id);
setViewPickerMode('favorite-folders-picker');
@@ -473,19 +473,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
});
}
const isNavigationMenuItemEnabled =
existingFeatureFlagsMap[FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED] ??
false;
const flatNavigationMenuItemToCreate = isNavigationMenuItemEnabled
? await this.computeFlatNavigationMenuItemToCreate({
view: flatDefaultViewToCreate,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
})
: null;
const flatNavigationMenuItemToCreate =
await this.computeFlatNavigationMenuItemToCreate({
view: flatDefaultViewToCreate,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -592,12 +587,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
if (!isNavigationMenuItemEnabled) {
await this.createWorkspaceFavoriteForNewObjectDefaultView({
view: flatDefaultViewToCreate,
workspaceId,
});
}
await this.createWorkspaceFavoriteForNewObjectDefaultView({
view: flatDefaultViewToCreate,
workspaceId,
});
return createdFlatObjectMetadata;
}
@@ -4,7 +4,6 @@ export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,