refactor(twenty-front): unify Favorites and Workspace navigation menu item code (#18697)
## Summary - Consolidate duplicated Favorites and Workspace navigation menu item frontend code into a single unified codebase within `navigation-menu-item/` - Move all DnD-related code from `navigation/` module into `navigation-menu-item/display/dnd/`, renaming `workspaceDndKit*` files to `navigationMenuItemDndKit*` - Unify duplicated components (DnD providers, DnD hooks, folder components, orphan items, section shell) using a `NavigationSections` enum to parameterize section-specific behavior - Rename residual workspace-prefixed symbols (`WorkspaceDndKitSortableItem`, `WorkspaceDndKitDroppableSlot`, `useWorkspaceSectionItems`, etc.) to `navigationMenuItem`-prefixed equivalents - Clean `navigation/` module to only contain app-level concerns (drawer layout, settings, routing) ### Key changes | Before (duplicated) | After (unified) | |---|---| | `FavoritesDndKitProvider` + `WorkspaceDndKitProvider` | `NavigationMenuItemDndKitProvider` with `section` prop | | `useFavoritesDndKit` + `useWorkspaceDndKit` | `useNavigationMenuItemDndKit(section)` | | `FavoritesFolderItem` + `WorkspaceNavigationMenuItemsFolder` | `NavigationMenuItemFolder` with `section` prop | | `FavoritesOrphanItems` | `NavigationMenuItemOrphanItems` with `section` prop | | Separate section shells | Shared `NavigationMenuItemSection` with thin wrappers | | `WorkspaceDndKitSortableItem` | `NavigationMenuItemSortableItem` | | `WorkspaceDndKitDroppableSlot` | `NavigationMenuItemDroppableSlot` | | `useWorkspaceSectionItems` | `useNavigationMenuItemSectionItems` | | `useWorkspaceFolderOpenState` | `useNavigationMenuItemFolderOpenState` | Net result: **~1650 lines deleted** across 51 files. ## Test plan - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx lint twenty-front` passes (0 errors) - [x] `npx nx test twenty-front` passes (763 suites, 4467 tests) - [ ] Smoke test: Favorites section renders, DnD reorder works, folder create/rename/delete works - [ ] Smoke test: Workspace section renders, edit mode works, DnD reorder works - [ ] Smoke test: Add-to-navigation from side panel works for both sections Made with [Cursor](https://cursor.com)
This commit is contained in:
+18
-8
@@ -66,11 +66,10 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(30 + (20 - 30) / 2));
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
|
||||
it('should round fractional midpoints', () => {
|
||||
it('should return fractional midpoints for adjacent positions', () => {
|
||||
const items = createItems([1, 2, 5]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
@@ -79,8 +78,7 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(5 + (2 - 5) / 2));
|
||||
expect(result).toBe(4);
|
||||
expect(result).toBe(3.5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,11 +92,10 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(20 - (20 - 10) / 2));
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
|
||||
it('should round fractional midpoints', () => {
|
||||
it('should return fractional midpoints for adjacent positions', () => {
|
||||
const items = createItems([1, 4, 10]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
@@ -107,8 +104,21 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(4 - (4 - 1) / 2));
|
||||
expect(result).toBe(3);
|
||||
expect(result).toBe(2.5);
|
||||
});
|
||||
|
||||
it('should produce unique position for sequential integers', () => {
|
||||
const items = createItems([1, 2]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
destinationIndex: 1,
|
||||
sourceIndex: 2,
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(1.5);
|
||||
expect(result).not.toBe(items[0].position);
|
||||
expect(result).not.toBe(items[1].position);
|
||||
});
|
||||
|
||||
it('should handle destinationIndex equal to sourceIndex', () => {
|
||||
|
||||
+7
-8
@@ -18,18 +18,17 @@ export const calculateNewPosition = ({
|
||||
}
|
||||
|
||||
if (destinationIndex > sourceIndex) {
|
||||
return Math.round(
|
||||
return (
|
||||
items[destinationIndex].position +
|
||||
(items[destinationIndex - 1].position -
|
||||
items[destinationIndex].position) /
|
||||
2,
|
||||
(items[destinationIndex - 1].position -
|
||||
items[destinationIndex].position) /
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
return Math.round(
|
||||
return (
|
||||
items[destinationIndex].position -
|
||||
(items[destinationIndex].position -
|
||||
items[destinationIndex - 1].position) /
|
||||
2,
|
||||
(items[destinationIndex].position - items[destinationIndex - 1].position) /
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const FOLDER_DROPPABLE_IDS = {
|
||||
FOLDER_PREFIX: 'folder-',
|
||||
FOLDER_HEADER_PREFIX: 'folder-header-',
|
||||
} as const;
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { FOLDER_DROPPABLE_IDS } from './folderDroppableIds';
|
||||
|
||||
type ValidateAndExtractFolderIdParams = {
|
||||
droppableId: string;
|
||||
// TODO: Remove orphanDroppableId prop when deleting all favorites code
|
||||
orphanDroppableId: string;
|
||||
};
|
||||
|
||||
export const validateAndExtractFolderId = ({
|
||||
droppableId,
|
||||
orphanDroppableId,
|
||||
}: ValidateAndExtractFolderIdParams): string | null => {
|
||||
if (droppableId === orphanDroppableId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX)) {
|
||||
const folderId = droppableId.replace(
|
||||
FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
'',
|
||||
);
|
||||
if (!folderId)
|
||||
throw new CustomError(
|
||||
`Invalid folder header ID: ${droppableId}`,
|
||||
'INVALID_FOLDER_HEADER_ID',
|
||||
);
|
||||
return folderId;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(FOLDER_DROPPABLE_IDS.FOLDER_PREFIX)) {
|
||||
const folderId = droppableId.replace(
|
||||
FOLDER_DROPPABLE_IDS.FOLDER_PREFIX,
|
||||
'',
|
||||
);
|
||||
if (!folderId)
|
||||
throw new CustomError(
|
||||
`Invalid folder ID: ${droppableId}`,
|
||||
'INVALID_FOLDER_ID',
|
||||
);
|
||||
return folderId;
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
`Invalid droppable ID format: ${droppableId}`,
|
||||
'INVALID_DROPPABLE_ID_FORMAT',
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/Keyboa
|
||||
import { LayoutCustomizationBar } from '@/layout-customization/components/LayoutCustomizationBar';
|
||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||
import { PageDragDropProvider } from '@/navigation/components/PageDragDropProvider';
|
||||
import { PageDragDropProvider } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProvider';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { SignInAppNavigationDrawerMock } from '@/sign-in-background-mock/components/SignInAppNavigationDrawerMock';
|
||||
|
||||
Reference in New Issue
Block a user