Integrate NavigationMenuItem with feature flag support (#17268)

## Implement Navigation Menu Items Frontend

Implements the frontend for navigation menu items, the new system
replacing favorites.

### Changes
- Added GraphQL fragments and queries for navigation menu items
- Added hooks for managing navigation menu items (create, update,
delete, sorting, filtering)
- Updated components to use navigation menu items instead of favorites
- Added test coverage for utility functions

### Migration Note
The favorites and navigation menu item modules currently exist in
parallel. The favorites code will be removed once all data has been
migrated to navigation menu items.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Replaces Favorites with feature-flagged `NavigationMenuItem` across
frontend and backend, while keeping Favorites as fallback until
migration completes.
> 
> - UI: new `navigation-menu-item` components (folders, orphan items,
drag provider/droppable, icons, skeleton), dispatcher components to
switch from Favorites, and updated “Add to favorites” action to create
`NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED`
> - DnD: shared `validateAndExtractFolderId` and droppable id utils
moved to `ui/layout/draggable-list`; favorites DnD updated to use shared
utils
> - GraphQL (client): add fragments, queries, mutations, hooks
(create/update/delete/find), and generated types; added
`RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem`
> - Prefetch: new prefetch state/effect for navigation menu items; skip
favorites prefetch when flag enabled
> - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`),
resolver `targetRecordIdentifier` field, service logic to fetch record
identifiers with permission-aware access and image signing,
`getRecordImageIdentifier` util, entity relation to `view`, and
migration adding FK on `viewId`
> - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to
enums, dev seeder enables it; standard app seeds workspace navigation
menu items instead of favorites when flag on
> - Tests: add unit tests for sorting/labels/folder id and related utils
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c99746f08b9f84fc8cec4fcc3a7d7afb8ea92db7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
Abdul Rahman
2026-01-27 16:12:27 +05:30
committed by GitHub
parent c79f633f17
commit 2a8f834377
107 changed files with 4518 additions and 92 deletions
@@ -1,10 +1,12 @@
import { PrefetchRunFavoriteQueriesEffect } from '@/prefetch/components/PrefetchRunFavoriteQueriesEffect';
import { PrefetchRunNavigationMenuItemQueriesEffect } from '@/prefetch/components/PrefetchRunNavigationMenuItemQueriesEffect';
import React from 'react';
export const PrefetchDataProvider = ({ children }: React.PropsWithChildren) => {
return (
<>
<PrefetchRunFavoriteQueriesEffect />
<PrefetchRunNavigationMenuItemQueriesEffect />
{children}
</>
);
@@ -16,11 +16,16 @@ import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { FeatureFlagKey } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const PrefetchRunFavoriteQueriesEffect = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const showAuthModal = useShowAuthModal();
const isSettingsPage = useIsSettingsPage();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
@@ -55,14 +60,22 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
objectNameSingular: CoreObjectNameSingular.Favorite,
filter: findAllFavoritesOperationSignature.variables.filter,
recordGqlFields: findAllFavoritesOperationSignature.fields,
skip: showAuthModal || isSettingsPage || !isWorkspaceActive,
skip:
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
isNavigationMenuItemEnabled,
});
const { records: favoriteFolders } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
filter: findAllFavoriteFoldersOperationSignature.variables.filter,
recordGqlFields: findAllFavoriteFoldersOperationSignature.fields,
skip: showAuthModal || isSettingsPage || !isWorkspaceActive,
skip:
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
isNavigationMenuItemEnabled,
});
const setPrefetchFavoritesState = useRecoilCallback(
@@ -0,0 +1,71 @@
import { useEffect } from 'react';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import {
useFindManyNavigationMenuItemsQuery,
type NavigationMenuItem,
} from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const PrefetchRunNavigationMenuItemQueriesEffect = () => {
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
);
const showAuthModal = useShowAuthModal();
const isSettingsPage = useIsSettingsPage();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isWorkspaceActive =
currentWorkspace?.activationStatus === WorkspaceActivationStatus.ACTIVE;
const setIsPrefetchNavigationMenuItemsLoaded = useSetRecoilState(
prefetchIsLoadedFamilyState(PrefetchKey.AllNavigationMenuItems),
);
const { data, loading } = useFindManyNavigationMenuItemsQuery({
skip:
showAuthModal ||
isSettingsPage ||
!isWorkspaceActive ||
!isNavigationMenuItemEnabled,
fetchPolicy: 'cache-and-network',
});
const setPrefetchNavigationMenuItemsState = useRecoilCallback(
({ set, snapshot }) =>
(navigationMenuItems: NavigationMenuItem[]) => {
const existingNavigationMenuItems = snapshot
.getLoadable(prefetchNavigationMenuItemsState)
.getValue();
if (!isDeeplyEqual(existingNavigationMenuItems, navigationMenuItems)) {
set(prefetchNavigationMenuItemsState, navigationMenuItems);
}
},
[],
);
useEffect(() => {
if (!loading && isDefined(data?.navigationMenuItems)) {
setPrefetchNavigationMenuItemsState(data.navigationMenuItems);
setIsPrefetchNavigationMenuItemsLoaded(true);
}
}, [
data,
loading,
setPrefetchNavigationMenuItemsState,
setIsPrefetchNavigationMenuItemsLoaded,
]);
return <></>;
};