diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds.ts index 700a471393..0c390fe47d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds.ts @@ -1,5 +1,10 @@ export enum NavigationMenuItemDroppableIds { - ORPHAN_NAVIGATION_MENU_ITEMS = 'orphan-navigation-menu-items', + FAVORITE_DROPPABLE_PREFIX = 'favorite-', + FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS = 'favorite-orphan-navigation-menu-items', + FAVORITE_FOLDER_PREFIX = 'favorite-folder-', + FAVORITE_FOLDER_HEADER_PREFIX = 'favorite-folder-header-', + + WORKSPACE_DROPPABLE_PREFIX = 'workspace-', WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS = 'workspace-orphan-navigation-menu-items', WORKSPACE_FOLDER_PREFIX = 'workspace-folder-', WORKSPACE_FOLDER_HEADER_PREFIX = 'workspace-folder-header-', diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig.ts new file mode 100644 index 0000000000..28108b83f3 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig.ts @@ -0,0 +1,28 @@ +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; + +type SectionDroppableConfig = { + orphanDroppableId: string; + folderHeaderPrefix: string; + folderPrefix: string; +}; + +export const NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG: Record< + NavigationMenuItemSection, + SectionDroppableConfig +> = { + favorite: { + orphanDroppableId: + NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS, + folderHeaderPrefix: + NavigationMenuItemDroppableIds.FAVORITE_FOLDER_HEADER_PREFIX, + folderPrefix: NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX, + }, + workspace: { + orphanDroppableId: + NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, + folderHeaderPrefix: + NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX, + folderPrefix: NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX, + }, +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts index f707b1aaea..9d764442ca 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts @@ -10,6 +10,7 @@ export const useUpdateNavigationMenuItem = () => { UpdateNavigationMenuItemDocument, { refetchQueries: ['FindManyNavigationMenuItems'], + awaitRefetchQueries: false, }, ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/types/NavigationMenuItemSection.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/NavigationMenuItemSection.ts new file mode 100644 index 0000000000..b019c4c182 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/types/NavigationMenuItemSection.ts @@ -0,0 +1 @@ +export type NavigationMenuItemSection = 'workspace' | 'favorite'; diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData.ts similarity index 100% rename from packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData.ts diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination.ts similarity index 100% rename from packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination.ts diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts similarity index 100% rename from packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts similarity index 58% rename from packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts index 17b7d104f8..a447d2f6a7 100644 --- a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts @@ -1,4 +1,4 @@ -import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; +import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination'; export type SortableTargetDestination = { destination: DropDestination; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/getDropTargetIdFromDestination.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/getDropTargetIdFromDestination.test.ts index 457561e445..ac4ebbf592 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/getDropTargetIdFromDestination.test.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/getDropTargetIdFromDestination.test.ts @@ -1,37 +1,109 @@ import { getDropTargetIdFromDestination } from '@/navigation-menu-item/common/utils/getDropTargetIdFromDestination'; describe('getDropTargetIdFromDestination', () => { - it('should return null when destination is null or droppableId is not workspace', () => { - expect(getDropTargetIdFromDestination(null)).toBe(null); - expect( - getDropTargetIdFromDestination({ - droppableId: 'favorites-orphan', - index: 0, - }), - ).toBe(null); + describe('workspace section', () => { + it('should return null when destination is null or droppableId is not workspace', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'workspace', + destination: null, + }), + ).toBe(null); + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'workspace', + destination: { droppableId: 'favorite-orphan', index: 0 }, + }), + ).toBe(null); + }); + + it('should return workspace-orphan-index for workspace orphan droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'workspace', + destination: { + droppableId: 'workspace-orphan-navigation-menu-items', + index: 2, + }, + }), + ).toBe('workspace-orphan-2'); + }); + + it('should return workspace-folderId-index for workspace folder droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'workspace', + destination: { + droppableId: 'workspace-folder-folder-123', + index: 1, + }, + }), + ).toBe('workspace-folder-123-1'); + }); + + it('should return workspace-folderId-index for workspace folder header droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'workspace', + destination: { + droppableId: 'workspace-folder-header-folder-456', + index: 0, + }, + }), + ).toBe('workspace-folder-456-0'); + }); }); - it('should return workspace-orphan-index for workspace orphan droppable', () => { - const result = getDropTargetIdFromDestination({ - droppableId: 'workspace-orphan-navigation-menu-items', - index: 2, + describe('favorite section', () => { + it('should return null when destination is null or droppableId is not favorite', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'favorite', + destination: null, + }), + ).toBe(null); + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'favorite', + destination: { droppableId: 'workspace-orphan', index: 0 }, + }), + ).toBe(null); }); - expect(result).toBe('workspace-orphan-2'); - }); - it('should return workspace-folderId-index for workspace folder droppable', () => { - const result = getDropTargetIdFromDestination({ - droppableId: 'workspace-folder-folder-123', - index: 1, + it('should return favorite-orphan-index for favorite orphan droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'favorite', + destination: { + droppableId: 'favorite-orphan-navigation-menu-items', + index: 3, + }, + }), + ).toBe('favorite-orphan-3'); }); - expect(result).toBe('workspace-folder-123-1'); - }); - it('should return workspace-folderId-index for workspace folder header droppable', () => { - const result = getDropTargetIdFromDestination({ - droppableId: 'workspace-folder-header-folder-456', - index: 0, + it('should return favorite-folderId-index for favorite folder droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'favorite', + destination: { + droppableId: 'favorite-folder-abc-123', + index: 1, + }, + }), + ).toBe('favorite-abc-123-1'); + }); + + it('should return favorite-folderId-index for favorite folder header droppable', () => { + expect( + getDropTargetIdFromDestination({ + navigationMenuItemSection: 'favorite', + destination: { + droppableId: 'favorite-folder-header-def-456', + index: 0, + }, + }), + ).toBe('favorite-def-456-0'); }); - expect(result).toBe('workspace-folder-456-0'); }); }); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn.ts new file mode 100644 index 0000000000..10aeb22d3c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn.ts @@ -0,0 +1,23 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; + +export const canNavigationMenuItemBeDroppedIn = ({ + navigationMenuItemSection, + droppableId, +}: { + navigationMenuItemSection: NavigationMenuItemSection; + droppableId: string | null | undefined; +}): boolean => { + if (!isDefined(droppableId)) { + return false; + } + + const prefix = + navigationMenuItemSection === 'workspace' + ? NavigationMenuItemDroppableIds.WORKSPACE_DROPPABLE_PREFIX + : NavigationMenuItemDroppableIds.FAVORITE_DROPPABLE_PREFIX; + + return droppableId.startsWith(prefix); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/extractFolderIdFromDroppableId.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/extractFolderIdFromDroppableId.ts new file mode 100644 index 0000000000..e9327d0b8c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/extractFolderIdFromDroppableId.ts @@ -0,0 +1,24 @@ +import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; + +export const extractFolderIdFromDroppableId = ( + droppableId: string, + navigationMenuItemSection: NavigationMenuItemSection, +): string | null => { + const config = + NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection]; + + if (droppableId === config.orphanDroppableId) { + return null; + } + + if (droppableId.startsWith(config.folderHeaderPrefix)) { + return droppableId.slice(config.folderHeaderPrefix.length) || null; + } + + if (droppableId.startsWith(config.folderPrefix)) { + return droppableId.slice(config.folderPrefix.length) || null; + } + + return null; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getDropTargetIdFromDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getDropTargetIdFromDestination.ts index 47f79b4210..5ef1178f6e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getDropTargetIdFromDestination.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getDropTargetIdFromDestination.ts @@ -1,15 +1,42 @@ -import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; -import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId'; +import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; import type { DropResult } from '@hello-pangea/dnd'; -export const getDropTargetIdFromDestination = ( - destination: DropResult['destination'], -): string | null => { - if (!destination || !isWorkspaceDroppableId(destination.droppableId)) { +export const getDropTargetIdFromDestination = ({ + navigationMenuItemSection, + destination, +}: { + navigationMenuItemSection: NavigationMenuItemSection; + destination: DropResult['destination']; +}): string | null => { + if ( + !destination || + !canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection, + droppableId: destination.droppableId, + }) + ) { return null; } - const folderId = validateAndExtractWorkspaceFolderId(destination.droppableId); - const folderSegment = folderId ?? 'orphan'; - return `${NavigationSections.WORKSPACE}-${folderSegment}-${destination.index}`; + + const config = + NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection]; + const { droppableId, index } = destination; + + if (droppableId === config.orphanDroppableId) { + return `${navigationMenuItemSection}-orphan-${index}`; + } + + if (droppableId.startsWith(config.folderHeaderPrefix)) { + const folderId = droppableId.slice(config.folderHeaderPrefix.length); + return `${navigationMenuItemSection}-${folderId}-${index}`; + } + + if (droppableId.startsWith(config.folderPrefix)) { + const folderId = droppableId.slice(config.folderPrefix.length); + return `${navigationMenuItemSection}-${folderId}-${index}`; + } + + return null; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getFavoritesDropTargetIdFromDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getFavoritesDropTargetIdFromDestination.ts deleted file mode 100644 index 7d5a292150..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/getFavoritesDropTargetIdFromDestination.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; -import type { DropResult } from '@hello-pangea/dnd'; - -const FOLDER_PREFIX = 'folder-'; - -export const getFavoritesDropTargetIdFromDestination = ( - destination: DropResult['destination'], -): string | null => { - if (!destination) return null; - const { droppableId, index } = destination; - if ( - droppableId === NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS - ) { - return `${NavigationSections.FAVORITES}-orphan-${index}`; - } - if (droppableId.startsWith(FOLDER_PREFIX)) { - const folderId = droppableId.slice(FOLDER_PREFIX.length); - return `${NavigationSections.FAVORITES}-${folderId}-${index}`; - } - return null; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isWorkspaceDroppableId.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isWorkspaceDroppableId.ts deleted file mode 100644 index 9849108c6a..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isWorkspaceDroppableId.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; - -export const isWorkspaceDroppableId = ( - droppableId: string | null | undefined, -): boolean => { - if (!isDefined(droppableId)) { - return false; - } - return ( - droppableId === - NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS || - droppableId.startsWith( - NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX, - ) || - droppableId.startsWith( - NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX, - ) - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemDisplay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemDisplay.tsx index e33943e0f3..ef71897ef9 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemDisplay.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemDisplay.tsx @@ -1,17 +1,19 @@ import { NavigationMenuItemType } from 'twenty-shared/types'; -import { NavigationMenuItemFolderDisplay } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDisplay'; +import { NavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolder'; import { NavigationMenuItemLinkDisplay } from '@/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay'; import { NavigationMenuItemObjectDisplay } from '@/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay'; -import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps'; +import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps'; -type NavigationMenuItemDisplayProps = WorkspaceSectionItemContentProps; +type NavigationMenuItemDisplayProps = NavigationMenuItemSectionContentProps; export const NavigationMenuItemDisplay = ({ item, + isEditInPlace, editModeProps, isDragging, folderChildrenById, folderCount, + rightOptions, selectedNavigationMenuItemId, onNavigationMenuItemClick, onActiveObjectMetadataItemClick, @@ -20,8 +22,9 @@ export const NavigationMenuItemDisplay = ({ switch (item.type) { case NavigationMenuItemType.FOLDER: return ( - ` min-height: 0; @@ -16,7 +16,7 @@ const SLOT_COLLISION_PRIORITY = 1; export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 2; -type WorkspaceDndKitDroppableSlotProps = { +type NavigationMenuItemDroppableSlotProps = { droppableId: string; index: number; children?: ReactNode; @@ -24,13 +24,13 @@ type WorkspaceDndKitDroppableSlotProps = { collisionPriority?: number; }; -export const WorkspaceDndKitDroppableSlot = ({ +export const NavigationMenuItemDroppableSlot = ({ droppableId, index, children, disabled = false, collisionPriority = SLOT_COLLISION_PRIORITY, -}: WorkspaceDndKitDroppableSlotProps) => { +}: NavigationMenuItemDroppableSlotProps) => { const id = getDndKitDropTargetId(droppableId, index); const data: DroppableData = { droppableId, index }; const { ref } = useDroppable({ diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem.tsx similarity index 90% rename from packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem.tsx index 8a45bfbf38..3afbc0802d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem.tsx @@ -14,7 +14,7 @@ const StyledSortableRoot = styled.div` position: relative; `; -type WorkspaceDndKitSortableItemProps = { +type NavigationMenuItemSortableItemProps = { children: ReactNode; disabled?: boolean; group: string; @@ -22,13 +22,13 @@ type WorkspaceDndKitSortableItemProps = { index: number; }; -export const WorkspaceDndKitSortableItem = ({ +export const NavigationMenuItemSortableItem = ({ id, index, group, disabled = false, children, -}: WorkspaceDndKitSortableItemProps) => { +}: NavigationMenuItemSortableItemProps) => { const { handleRef, ref, targetRef } = useSortable({ id, index, diff --git a/packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions.ts similarity index 100% rename from packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions.ts diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts index 3cee4c9434..6b50cdb59a 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts @@ -19,7 +19,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft'; import { getStandardObjectIconColor } from '@/navigation-menu-item/common/utils/getStandardObjectIconColor'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -54,7 +54,10 @@ export const useHandleAddToNavigationDrop = () => { if ( source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID || !destination || - !isWorkspaceDroppableId(destination.droppableId) + !canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: destination.droppableId, + }) ) { return; } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts index 9962f5b96d..51178691e3 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts @@ -1,14 +1,13 @@ import { type OnDragEndResponder } from '@hello-pangea/dnd'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition'; -import { FOLDER_DROPPABLE_IDS } from '@/ui/layout/draggable-list/utils/folderDroppableIds'; -import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId'; +import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; @@ -49,7 +48,12 @@ export const useHandleNavigationMenuItemDragAndDrop = () => { return; } - if (isWorkspaceDroppableId(destination.droppableId)) { + if ( + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: destination.droppableId, + }) + ) { return; } @@ -60,20 +64,18 @@ export const useHandleNavigationMenuItemDragAndDrop = () => { return; } - const destinationFolderId = validateAndExtractFolderId({ - droppableId: destination.droppableId, - orphanDroppableId: - NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS, - }); - const sourceFolderId = validateAndExtractFolderId({ - droppableId: source.droppableId, - orphanDroppableId: - NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS, - }); + const destinationFolderId = extractFolderIdFromDroppableId( + destination.droppableId, + 'favorite', + ); + const sourceFolderId = extractFolderIdFromDroppableId( + source.droppableId, + 'favorite', + ); if ( destination.droppableId.startsWith( - FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX, + NavigationMenuItemDroppableIds.FAVORITE_FOLDER_HEADER_PREFIX, ) ) { if (destinationFolderId === null) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection.ts index d7493da4fc..bebbc132a4 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection.ts @@ -1,6 +1,6 @@ import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -12,5 +12,9 @@ export const useIsDropDisabledForSection = (isWorkspaceSection: boolean) => { if (sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { return !isWorkspaceSection; } - return isWorkspaceDroppableId(sourceDroppableId) !== isWorkspaceSection; + const sourceIsWorkspace = canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: sourceDroppableId, + }); + return sourceIsWorkspace !== isWorkspaceSection; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit.ts new file mode 100644 index 0000000000..ab93fa622d --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit.ts @@ -0,0 +1,426 @@ +import { type DragDropProvider } from '@dnd-kit/react'; +import { isSortable } from '@dnd-kit/react/sortable'; +import type { ResponderProvided } from '@hello-pangea/dnd'; +import { useStore } from 'jotai'; +import { type ComponentProps, useCallback, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; +import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; +import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData'; +import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; +import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { DROP_RESULT_OPTIONS } from '@/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions'; +import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop'; +import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop'; +import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop'; +import { resolveDropTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget'; +import { toDropResult } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult'; +import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; +import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; +import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; + +import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; +import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination'; + +type DragStartPayload = Parameters< + NonNullable< + ComponentProps>['onDragStart'] + > +>[0]; +type DragOverPayload = Parameters< + NonNullable< + ComponentProps>['onDragOver'] + > +>[0]; +type DragEndPayload = Parameters< + NonNullable< + ComponentProps>['onDragEnd'] + > +>[0]; + +export type NavigationMenuItemDndKitContextValues = { + dragSource: { sourceDroppableId: string | null }; + drag: { isDragging: boolean }; + dropTarget: { + activeDropTargetId: string | null; + setActiveDropTargetId: (id: string | null) => void; + forbiddenDropTargetId: string | null; + setForbiddenDropTargetId: (id: string | null) => void; + addToNavigationFallbackDestination: DropDestination | null; + }; +}; + +export const useNavigationMenuItemDndKit = ( + section: NavigationSections, +): { + contextValues: NavigationMenuItemDndKitContextValues; + handlers: { + onDragStart: (event: DragStartPayload) => void; + onDragOver: (event: DragOverPayload) => void; + onDragEnd: (event: DragEndPayload) => void; + }; +} => { + const sectionType: NavigationMenuItemSection = + section === NavigationSections.FAVORITES ? 'favorite' : 'workspace'; + const isWorkspaceSection = sectionType === 'workspace'; + + const store = useStore(); + + const [isDragging, setIsDragging] = useState(false); + const [sourceDroppableId, setSourceDroppableId] = useState( + null, + ); + const [activeDropTargetId, setActiveDropTargetId] = useState( + null, + ); + const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< + string | null + >(null); + const [ + addToNavigationFallbackDestination, + setAddToNavigationFallbackDestination, + ] = useState(null); + + const { navigationMenuItems } = useNavigationMenuItemsData(); + const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); + const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); + const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop(); + const { handleNavigationMenuItemDragAndDrop } = + useHandleNavigationMenuItemDragAndDrop(); + const { handleWorkspaceNavigationMenuItemDragAndDrop } = + useHandleWorkspaceNavigationMenuItemDragAndDrop(); + + const items = isWorkspaceSection + ? workspaceNavigationMenuItems + : navigationMenuItems; + + const orphanItems = isWorkspaceSection + ? workspaceNavigationMenuItems + : navigationMenuItemsSorted; + + const orphanItemCount = orphanItems.filter( + (item: { folderId?: string | null }) => !isDefined(item.folderId), + ).length; + + const { orphanDroppableId: defaultOrphanDroppableId } = + NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[sectionType]; + + const getNavItemById = useCallback( + (id: string | undefined) => + id ? items.find((item) => item.id === id) : undefined, + [items], + ); + + const getAddToNavPayload = useCallback( + (sourceId: unknown) => + store.get(addToNavPayloadRegistryState.atom).get(String(sourceId)) ?? + null, + [store], + ); + + const isSourceFolderDrag = useCallback( + (source: { id?: unknown; data?: unknown } | null): boolean => { + const sourceItem = getNavItemById( + source?.id != null ? String(source.id) : undefined, + ); + if (isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem)) { + return true; + } + const payload = getAddToNavPayload(source?.id); + return payload?.type === 'FOLDER'; + }, + [getNavItemById, getAddToNavPayload], + ); + + const computeForbiddenTargetId = useCallback( + ( + source: { id?: unknown; data?: unknown } | null, + resolved: SortableTargetDestination, + isAddToNavDrag: boolean, + ): string | null => { + const sourceIsFolder = isAddToNavDrag + ? getAddToNavPayload(source?.id)?.type === 'FOLDER' + : isSourceFolderDrag(source); + + if (!sourceIsFolder) { + return null; + } + + if (resolved.isTargetFolder) { + return resolved.effectiveDropTargetId; + } + + const destFolderId = extractFolderIdFromDroppableId( + resolved.destination.droppableId, + sectionType, + ); + if (isDefined(destFolderId)) { + return resolved.dropTargetId; + } + + return null; + }, + [sectionType, getAddToNavPayload, isSourceFolderDrag], + ); + + const applyWorkspaceReorderIfAllowed = useCallback( + ( + id: string, + source: DropDestination, + destination: DropDestination, + insertBeforeItemId?: string | null, + ) => { + const draggedItem = getNavItemById(id); + const destFolderId = extractFolderIdFromDroppableId( + destination.droppableId, + 'workspace', + ); + if ( + isDefined(destFolderId) && + isDefined(draggedItem) && + isNavigationMenuItemFolder(draggedItem) + ) { + return; + } + const result = toDropResult( + id, + { + sourceDroppableId: source.droppableId, + sourceIndex: source.index, + }, + destination, + ); + const provided: ResponderProvided = { announce: () => {} }; + handleWorkspaceNavigationMenuItemDragAndDrop( + { + ...result, + ...DROP_RESULT_OPTIONS, + ...(insertBeforeItemId != null && { insertBeforeItemId }), + }, + provided, + ); + }, + [getNavItemById, handleWorkspaceNavigationMenuItemDragAndDrop], + ); + + const handleDragStart = (event: DragStartPayload) => { + const { operation } = event; + setIsDragging(true); + const source = operation.source; + const sourceId = source?.data?.sourceDroppableId ?? null; + setSourceDroppableId(sourceId); + + if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { + const defaultDestination: DropDestination = { + droppableId: defaultOrphanDroppableId, + index: orphanItemCount, + }; + setAddToNavigationFallbackDestination(defaultDestination); + setActiveDropTargetId( + getDndKitDropTargetId( + defaultDestination.droppableId, + defaultDestination.index, + ), + ); + } + }; + + const handleDragOver = useCallback( + (event: DragOverPayload) => { + const { operation } = event; + const source = operation.source; + const target = operation.target; + const isAddToNavDrag = + sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; + const sourceIsSortable = source !== null && isSortable(source); + const resolved = resolveDropTarget(target, getNavItemById, sectionType); + + // Branch 1: sortable-to-sortable + if ( + resolved !== null && + source !== null && + target !== null && + isSortable(source) && + isSortable(target) + ) { + setActiveDropTargetId(resolved.effectiveDropTargetId); + setForbiddenDropTargetId( + isAddToNavDrag + ? computeForbiddenTargetId(source, resolved, true) + : computeForbiddenTargetId(source, resolved, false), + ); + return; + } + + // Branch 2: sortable-to-droppable-slot + if (resolved !== null && sourceIsSortable) { + setActiveDropTargetId(resolved.effectiveDropTargetId); + setAddToNavigationFallbackDestination(resolved.destination); + setForbiddenDropTargetId( + computeForbiddenTargetId(source, resolved, isAddToNavDrag), + ); + return; + } + + if (!isAddToNavDrag) { + return; + } + + // Branch 3: add-to-nav drag + if (resolved !== null) { + setAddToNavigationFallbackDestination(resolved.destination); + setActiveDropTargetId(resolved.effectiveDropTargetId); + setForbiddenDropTargetId( + computeForbiddenTargetId(source, resolved, true), + ); + return; + } + + const fallback = addToNavigationFallbackDestination; + setActiveDropTargetId( + fallback + ? getDndKitDropTargetId(fallback.droppableId, fallback.index) + : null, + ); + setForbiddenDropTargetId(null); + }, + [ + sourceDroppableId, + addToNavigationFallbackDestination, + getNavItemById, + sectionType, + computeForbiddenTargetId, + ], + ); + + const handleDragEnd = (event: DragEndPayload) => { + const { operation } = event; + const source = operation.source; + const target = operation.target; + const draggableId = String(source?.id); + const data = source?.data; + const sourceId = data?.sourceDroppableId ?? null; + const fallback = addToNavigationFallbackDestination; + + setIsDragging(false); + setSourceDroppableId(null); + setActiveDropTargetId(null); + setForbiddenDropTargetId(null); + setAddToNavigationFallbackDestination(null); + + const sourceIsSortable = source !== null && isSortable(source); + const targetIsSortable = target !== null && isSortable(target); + const resolved = resolveDropTarget(target, getNavItemById, sectionType); + + // Workspace fast path: sortable-to-sortable within workspace + if ( + isWorkspaceSection && + sourceIsSortable && + targetIsSortable && + isDefined(source) && + isDefined(target) && + resolved !== null + ) { + const sourceDraggable = 'initialGroup' in source ? source : null; + const initialGroup = String(sourceDraggable?.initialGroup ?? ''); + const initialIndex = sourceDraggable?.initialIndex ?? 0; + const destGroup = String(target.group ?? ''); + const bothWorkspace = + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: initialGroup, + }) && + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: destGroup, + }); + if (bothWorkspace) { + const insertBeforeItemId = resolved.isTargetFolder + ? null + : String(target?.id ?? ''); + applyWorkspaceReorderIfAllowed( + draggableId, + { droppableId: initialGroup, index: initialIndex }, + resolved.destination, + insertBeforeItemId || undefined, + ); + return; + } + } + + let destination: DropDestination | null = resolved?.destination ?? null; + if ( + destination == null && + isDefined(fallback) && + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: sectionType, + droppableId: fallback.droppableId, + }) + ) { + destination = fallback; + } + + const result = toDropResult(draggableId, data, destination); + const provided: ResponderProvided = { announce: () => {} }; + const dropResult = { ...result, ...DROP_RESULT_OPTIONS }; + + if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { + handleAddToNavigationDrop(dropResult, provided); + return; + } + + if (isWorkspaceSection) { + if ( + isDefined(sourceId) && + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: sourceId, + }) && + isDefined(destination) && + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection: 'workspace', + droppableId: destination.droppableId, + }) + ) { + applyWorkspaceReorderIfAllowed( + draggableId, + { + droppableId: data?.sourceDroppableId ?? '', + index: data?.sourceIndex ?? 0, + }, + destination, + ); + } + return; + } + + handleNavigationMenuItemDragAndDrop(dropResult, provided); + }; + + const contextValues: NavigationMenuItemDndKitContextValues = { + dragSource: { sourceDroppableId }, + drag: { isDragging }, + dropTarget: { + activeDropTargetId, + setActiveDropTargetId, + forbiddenDropTargetId, + setForbiddenDropTargetId, + addToNavigationFallbackDestination, + }, + }; + + return { + contextValues, + handlers: { + onDragStart: handleDragStart, + onDragOver: handleDragOver, + onDragEnd: handleDragEnd, + }, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit.ts new file mode 100644 index 0000000000..be10725db1 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit.ts @@ -0,0 +1,18 @@ +let preloadScheduled = false; + +const preload = () => { + void import( + '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider' + ); + void import( + '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit' + ); +}; + +export const preloadNavigationMenuItemDndKit = (): void => { + if (preloadScheduled) { + return; + } + preloadScheduled = true; + preload(); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/FavoritesDndKitProvider.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider.tsx similarity index 66% rename from packages/twenty-front/src/modules/navigation/components/FavoritesDndKitProvider.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider.tsx index 86a1f6ceb0..7ce6886e34 100644 --- a/packages/twenty-front/src/modules/navigation/components/FavoritesDndKitProvider.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider.tsx @@ -6,14 +6,14 @@ import { } from '@dnd-kit/react'; import type { ReactNode } from 'react'; +import type { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext'; import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; +import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData'; +import { useNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit'; -import { useFavoritesDndKit } from '@/navigation/hooks/useFavoritesDndKit'; - -const FAVORITES_DND_SENSORS = [ +const NAVIGATION_MENU_ITEM_DND_SENSORS = [ PointerSensor.configure({ activationConstraints: [ new PointerActivationConstraints.Distance({ value: 8 }), @@ -22,21 +22,23 @@ const FAVORITES_DND_SENSORS = [ KeyboardSensor, ]; -type FavoritesDndKitProviderProps = { +type NavigationMenuItemDndKitProviderProps = { + section: NavigationSections; children: ReactNode; }; -export const FavoritesDndKitProvider = ({ +export const NavigationMenuItemDndKitProvider = ({ + section, children, -}: FavoritesDndKitProviderProps) => { - const { contextValues, handlers } = useFavoritesDndKit(); +}: NavigationMenuItemDndKitProviderProps) => { + const { contextValues, handlers } = useNavigationMenuItemDndKit(section); return ( - sensors={FAVORITES_DND_SENSORS} + sensors={NAVIGATION_MENU_ITEM_DND_SENSORS} onDragStart={handlers.onDragStart} onDragOver={handlers.onDragOver} onDragEnd={handlers.onDragEnd} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/PageDragDropProvider.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/PageDragDropProvider.tsx new file mode 100644 index 0000000000..379ab4f8e5 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/PageDragDropProvider.tsx @@ -0,0 +1,43 @@ +import { lazy, Suspense, useState, type ReactNode } from 'react'; + +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; +import { PageDragDropProviderMountEffect } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProviderMountEffect'; + +const LazyNavigationMenuItemDndKitProvider = lazy(() => + import( + '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider' + ).then((m) => ({ + default: m.NavigationMenuItemDndKitProvider, + })), +); + +type PageDragDropProviderProps = { + children: ReactNode; +}; + +export const PageDragDropProvider = ({ + children, +}: PageDragDropProviderProps) => { + const [hasProviderMounted, setHasProviderMounted] = useState(false); + + if (!hasProviderMounted) { + return ( + <> + setHasProviderMounted(true)} + /> + {children} + + ); + } + + return ( + {children}}> + + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/PageDragDropProviderMountEffect.tsx similarity index 100% rename from packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/providers/PageDragDropProviderMountEffect.tsx diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts similarity index 64% rename from packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts index 9719b8ac9b..511d65bd07 100644 --- a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts @@ -1,13 +1,13 @@ import { isDefined } from 'twenty-shared/utils'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; +import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; +import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination'; +import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination'; import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; -import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination'; - type GetNavItemById = ( id: string | undefined, ) => NavigationMenuItem | undefined; @@ -15,6 +15,7 @@ type GetNavItemById = ( export const getDestinationFromSortableTarget = ( target: { id: unknown; group?: unknown; index?: unknown }, getNavItemById: GetNavItemById, + navigationMenuItemSection: NavigationMenuItemSection, ): SortableTargetDestination | null => { const group = target.group; const rawIndex = target.index; @@ -32,15 +33,16 @@ export const getDestinationFromSortableTarget = ( const isTargetFolder = isDefined(targetItem) && isNavigationMenuItemFolder(targetItem); const dropTargetId = getDndKitDropTargetId(destDroppableId, index); + + const { folderHeaderPrefix } = + NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection]; + const effectiveDropTargetId = isTargetFolder - ? getDndKitDropTargetId( - `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`, - 0, - ) + ? getDndKitDropTargetId(`${folderHeaderPrefix}${target.id}`, 0) : dropTargetId; const destination: DropDestination = { droppableId: isTargetFolder - ? `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}` + ? `${folderHeaderPrefix}${target.id}` : destDroppableId, index: isTargetFolder ? 0 : index, }; diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts similarity index 59% rename from packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts index 04adcc7ae1..1319f8c6d1 100644 --- a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts @@ -1,11 +1,11 @@ import { isDefined } from 'twenty-shared/utils'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; - -import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData'; -import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination'; -import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; +import type { DroppableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData'; +import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; +import { getDestinationFromSortableTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget'; type GetNavItemById = ( id: string | undefined, @@ -25,6 +25,7 @@ export const resolveDropTarget = ( data?: unknown; } | null, getNavItemById: GetNavItemById, + navigationMenuItemSection: NavigationMenuItemSection, ): SortableTargetDestination | null => { if (target === null || target === undefined) { return null; @@ -33,11 +34,17 @@ export const resolveDropTarget = ( return getDestinationFromSortableTarget( { id: target.id, group: target.group, index: target.index }, getNavItemById, + navigationMenuItemSection, ); } if (isDroppableData(target.data)) { const { droppableId, index } = target.data; - if (isWorkspaceDroppableId(droppableId)) { + if ( + canNavigationMenuItemBeDroppedIn({ + navigationMenuItemSection, + droppableId, + }) + ) { return { destination: { droppableId, index }, effectiveDropTargetId: String(target.id), diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult.ts similarity index 66% rename from packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult.ts index 30e951e043..d701c73d1b 100644 --- a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult.ts @@ -1,5 +1,5 @@ -import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; -import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; +import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination'; +import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData'; export const toDropResult = ( draggableId: string, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx new file mode 100644 index 0000000000..a0e755ba7c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx @@ -0,0 +1,169 @@ +import { Suspense, lazy } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { useIsMobile } from 'twenty-ui/utilities'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; +import { NavigationMenuItemFolderLayout } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout'; +import { NavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem'; +import { useNavigationMenuItemFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState'; +import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; + +const LazyNavigationMenuItemFolderDnd = lazy(() => + import( + '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd' + ).then((module) => ({ default: module.NavigationMenuItemFolderDnd })), +); + +type NavigationMenuItemFolderProps = Pick< + NavigationMenuItemSectionContentProps, + | 'item' + | 'isEditInPlace' + | 'editModeProps' + | 'isDragging' + | 'folderChildrenById' + | 'folderCount' + | 'selectedNavigationMenuItemId' + | 'onNavigationMenuItemClick' + | 'readOnly' +>; + +export const NavigationMenuItemFolder = ({ + item, + isEditInPlace = false, + editModeProps, + isDragging, + folderChildrenById, + folderCount, + selectedNavigationMenuItemId, + onNavigationMenuItemClick, + readOnly = false, +}: NavigationMenuItemFolderProps) => { + const folderId = item.id; + const folderName = item.name ?? 'Folder'; + const folderIconKey = item.icon; + const folderColor = 'color' in item ? (item.color as string | null) : null; + const navigationMenuItems = folderChildrenById.get(folderId) ?? []; + const isGroup = folderCount > 1; + + if (readOnly) { + return ( + + ); + } + + return ( + + } + > + + + ); +}; + +type NavigationMenuItemFolderReadOnlyContentProps = { + folderId: string; + folderName: string; + folderIconKey?: string | null; + folderColor?: string | null; + navigationMenuItems: NavigationMenuItem[]; + isGroup: boolean; +}; + +const NavigationMenuItemFolderReadOnlyContent = ({ + folderId, + folderName, + folderIconKey, + folderColor, + navigationMenuItems, + isGroup, +}: NavigationMenuItemFolderReadOnlyContentProps) => { + const { getIcon } = useIcons(); + const isMobile = useIsMobile(); + const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); + + const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = + useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems }); + + return ( + = 0} + onClick={handleToggle} + className="navigation-drawer-item" + triggerEvent="CLICK" + preventCollapseOnMobile={isMobile} + alwaysShowRightOptions + rightOptions={ + isOpen ? ( + + ) : ( + + ) + } + /> + } + isOpen={isOpen} + isGroup={isGroup} + > + {navigationMenuItems.map((navigationMenuItem, index) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx index 46bdc03cc0..7826c188ba 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx @@ -2,9 +2,10 @@ import { IconHeartOff } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; import { NavigationMenuItemBackButton } from '@/navigation-menu-item/edit/components/NavigationMenuItemBackButton'; import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; -import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem'; +import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; @@ -30,7 +31,7 @@ export const NavigationMenuItemFolderContent = ({ const views = useAtomStateValue(viewsSelector); const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); - const folderDroppableId = `folder-${folderId}`; + const folderDroppableId = `${NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX}${folderId}`; return ( <> @@ -53,7 +54,7 @@ export const NavigationMenuItemFolderContent = ({ ); return ( - - + ); })} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDisplay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDisplay.tsx deleted file mode 100644 index cf20662fd0..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDisplay.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { lazy, Suspense } from 'react'; - -import { NavigationMenuItemFolderReadOnly } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderReadOnly'; -import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps'; - -const LazyWorkspaceNavigationMenuItemsFolder = lazy(() => - import( - '@/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder' - ).then((m) => ({ default: m.WorkspaceNavigationMenuItemsFolder })), -); - -type NavigationMenuItemFolderDisplayProps = WorkspaceSectionItemContentProps; - -export const NavigationMenuItemFolderDisplay = ({ - item, - editModeProps, - isDragging, - folderChildrenById, - folderCount, - selectedNavigationMenuItemId, - onNavigationMenuItemClick, - readOnly = false, -}: NavigationMenuItemFolderDisplayProps) => { - const folderId = item.id; - const folderName = item.name ?? 'Folder'; - const folderIconKey = item.icon; - const folderColor = 'color' in item ? item.color : undefined; - const navigationMenuItems = folderChildrenById.get(item.id) ?? []; - const isGroup = folderCount > 1; - - if (readOnly) { - return ( - - ); - } - return ( - - } - > - - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx new file mode 100644 index 0000000000..8492214273 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx @@ -0,0 +1,404 @@ +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import React, { Fragment, useCallback, useContext } from 'react'; +import { createPortal } from 'react-dom'; +import { SidePanelPages } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { + IconChevronDown, + IconChevronRight, + IconFolder, + IconHeartOff, + IconPlus, + useIcons, +} from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; +import { useIsMobile } from 'twenty-ui/utilities'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; +import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; +import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; +import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; +import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext'; +import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState'; +import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; +import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget'; +import { + FOLDER_HEADER_SLOT_COLLISION_PRIORITY, + NavigationMenuItemDroppableSlot, +} from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot'; +import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; +import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection'; +import { NavigationMenuItemFolderLayout } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout'; +import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderNavigationDrawerItemDropdown'; +import { NavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem'; +import { useNavigationMenuItemFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState'; +import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; +import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage'; +import { useFavoritesFolderEdit } from '@/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit'; +import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; +import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +const StyledFolderContainer = styled.div<{ + $isSelectedInEditMode: boolean; +}>` + border: ${({ $isSelectedInEditMode }) => + $isSelectedInEditMode + ? `1px solid ${themeCssVariables.color.blue}` + : '1px solid transparent'}; + border-radius: ${themeCssVariables.border.radius.sm}; + transition: background-color 150ms ease-in-out; + + &[data-drag-over-header='true'] { + background-color: ${themeCssVariables.background.transparent.blue}; + } + + &[data-forbidden-drop-target='true'] { + background-color: ${themeCssVariables.background.transparent.danger}; + } +`; + +const StyledFolderDroppableContent = styled.div` + display: flex; + flex-direction: column; +`; + +type NavigationMenuItemFolderDndProps = { + folderId: string; + folderName: string; + folderIconKey?: string | null; + folderColor?: string | null; + navigationMenuItems: NavigationMenuItem[]; + isGroup: boolean; + isEditInPlace: boolean; + editModeProps?: EditModeProps; + isDragging: boolean; + selectedNavigationMenuItemId?: string | null; + onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; +}; + +export const NavigationMenuItemFolderDnd = ({ + folderId, + folderName, + folderIconKey, + folderColor, + navigationMenuItems, + isGroup, + isEditInPlace, + editModeProps, + isDragging: isDraggingProp, + selectedNavigationMenuItemId, + onNavigationMenuItemClick, +}: NavigationMenuItemFolderDndProps) => { + const { t } = useLingui(); + const { theme } = useContext(ThemeContext); + const { getIcon } = useIcons(); + const isMobile = useIsMobile(); + + const section: NavigationMenuItemSection = isEditInPlace + ? 'favorite' + : 'workspace'; + const isWorkspace = !isEditInPlace; + const sectionId = isEditInPlace + ? NavigationSections.FAVORITES + : NavigationSections.WORKSPACE; + + const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = + useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems }); + + const { isDragging: isContextDragging } = useContext( + NavigationMenuItemDragContext, + ); + const isDragging = isDraggingProp || isContextDragging; + + const setSortableDropTargetRef = useContext(SortableDropTargetRefContext); + const dropDisabled = useIsDropDisabledForSection(isWorkspace); + const { activeDropTargetId, forbiddenDropTargetId } = useContext( + NavigationDropTargetContext, + ); + + const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const favoritesEdit = useFavoritesFolderEdit({ + folderId, + folderName, + navigationMenuItems, + }); + + const isLayoutCustomizationModeEnabled = useAtomStateValue( + isLayoutCustomizationModeEnabledState, + ); + const { openAddItemToFolderPage } = useOpenAddItemToFolderPage(); + const sidePanelPage = useAtomStateValue(sidePanelPageState); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); + + const config = NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[section]; + const folderHeaderDroppableId = `${config.folderHeaderPrefix}${folderId}`; + const folderContentDroppableId = `${config.folderPrefix}${folderId}`; + const folderHeaderSlotId = getDndKitDropTargetId(folderHeaderDroppableId, 0); + + const isForbiddenDropTarget = + isDefined(forbiddenDropTargetId) && + (forbiddenDropTargetId.startsWith(`${folderContentDroppableId}::`) || + forbiddenDropTargetId.startsWith(`${folderHeaderDroppableId}::`)); + const isDragOverFolderHeader = + !isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId; + + const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); + const iconColor = isDefined(folderColor) + ? folderColor + : DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER; + + const isSelectedInEditMode = editModeProps?.isSelectedInEditMode ?? false; + const shouldUseEditModeClick = + isWorkspace && + isLayoutCustomizationModeEnabled && + isDefined(editModeProps?.onEditModeClick); + + const handleHeaderClick = shouldUseEditModeClick + ? (event?: React.MouseEvent) => { + event?.stopPropagation(); + if (isSelectedInEditMode) { + handleToggle(); + } else { + editModeProps?.onEditModeClick?.(); + } + } + : handleToggle; + + const headerRightOptions = isEditInPlace ? ( + favoritesEdit.setIsRenaming(true)} + onDelete={favoritesEdit.handleFolderDelete} + closeDropdown={favoritesEdit.closeDropdown} + /> + ) : ( +
{ + event.stopPropagation(); + handleToggle(); + }} + > + {isOpen ? ( + + ) : ( + + )} +
+ ); + + const headerOverride = + isEditInPlace && favoritesEdit.isRenaming ? ( + + ) : undefined; + + const header = headerOverride ?? ( + = 0) || + (isWorkspace && isSelectedInEditMode && !isOpen) + } + onClick={handleHeaderClick} + rightOptions={headerRightOptions} + className="navigation-drawer-item" + isRightOptionsDropdownOpen={ + isEditInPlace ? favoritesEdit.isDropdownOpen : undefined + } + triggerEvent="CLICK" + preventCollapseOnMobile={isMobile} + isDragging={isDragging} + alwaysShowRightOptions + /> + ); + + const wrappedHeader = ( +
+ + {header} + +
+ ); + + const isCompact = isWorkspace + ? isLayoutCustomizationModeEnabled || navigationMenuItems.length === 0 + : true; + + const folderContentLength = + isWorkspace && isLayoutCustomizationModeEnabled + ? navigationMenuItems.length + 1 + : navigationMenuItems.length; + + const handleAddMenuItemToFolder = useCallback(() => { + openAddItemToFolderPage({ + targetFolderId: folderId, + targetIndex: navigationMenuItems.length, + resetNavigationStack: true, + }); + }, [folderId, navigationMenuItems.length, openAddItemToFolderPage]); + + const deleteModal = + isEditInPlace && favoritesEdit.isModalOpened + ? createPortal( + 1 + ? t`Remove ${favoritesEdit.navigationMenuItemCount} navigation menu items?` + : t`Remove ${favoritesEdit.navigationMenuItemCount} navigation menu item?` + } + subtitle={ + favoritesEdit.navigationMenuItemCount > 1 + ? t`This action will delete this folder and all ${favoritesEdit.navigationMenuItemCount} navigation menu items inside. Do you want to continue?` + : t`This action will delete this folder and the navigation menu item inside. Do you want to continue?` + } + onConfirmClick={favoritesEdit.handleConfirmDelete} + confirmButtonText={t`Delete Folder`} + />, + document.body, + ) + : null; + + return ( + <> + + + + {navigationMenuItems.map((navigationMenuItem, index) => ( + + + + { + event.stopPropagation(); + deleteNavigationMenuItem(navigationMenuItem.id); + }} + accent="tertiary" + /> + ) : undefined + } + onNavigationMenuItemClick={ + isWorkspace ? onNavigationMenuItemClick : undefined + } + selectedNavigationMenuItemId={ + isWorkspace ? selectedNavigationMenuItemId : undefined + } + /> + + + ))} + + + {isWorkspace && isLayoutCustomizationModeEnabled && ( + + )} + + + + + {deleteModal} + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout.tsx new file mode 100644 index 0000000000..b14afe258e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout.tsx @@ -0,0 +1,44 @@ +import { styled } from '@linaria/react'; +import { useState, type ReactNode } from 'react'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; + +import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer'; + +const StyledFolderExpandableWrapper = styled.div` + & > div { + overflow: visible !important; + } +`; + +type NavigationMenuItemFolderLayoutProps = { + header: ReactNode; + isOpen: boolean; + isGroup: boolean; + children: ReactNode; +}; + +export const NavigationMenuItemFolderLayout = ({ + header, + isOpen, + isGroup, + children, +}: NavigationMenuItemFolderLayoutProps) => { + const [skipInitialExpandAnimation] = useState(() => isOpen); + + return ( + + {header} + + + {children} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderReadOnly.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderReadOnly.tsx deleted file mode 100644 index 461c9b1eb4..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderReadOnly.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { styled } from '@linaria/react'; -import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display'; -import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { useIsMobile } from 'twenty-ui/utilities'; -import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { type NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem'; -import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; -import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; -import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; -import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer'; - -const StyledFolderContainer = styled.div` - border: 1px solid transparent; - border-radius: ${themeCssVariables.border.radius.sm}; -`; - -const StyledFolderContent = styled.div` - display: flex; - flex-direction: column; -`; - -const StyledFolderExpandableWrapper = styled.div` - & > div { - overflow: visible !important; - } -`; - -type NavigationMenuItemFolderReadOnlyProps = { - folderId: string; - folderName: string; - folderIconKey?: string | null; - folderColor?: string | null; - navigationMenuItems: NavigationMenuItem[]; - isGroup: boolean; -}; - -export const NavigationMenuItemFolderReadOnly = ({ - folderId, - folderName, - folderIconKey, - folderColor, - navigationMenuItems, - isGroup, -}: NavigationMenuItemFolderReadOnlyProps) => { - const { getIcon } = useIcons(); - const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); - const isMobile = useIsMobile(); - const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = - useWorkspaceFolderOpenState({ folderId, navigationMenuItems }); - - const [skipInitialExpandAnimation] = useState(() => isOpen); - - return ( - - - = 0} - onClick={handleToggle} - className="navigation-drawer-item" - triggerEvent="CLICK" - preventCollapseOnMobile={isMobile} - alwaysShowRightOptions - rightOptions={ - isOpen ? ( - - ) : ( - - ) - } - /> - - - - {navigationMenuItems.map((navigationMenuItem, index) => ( - - ))} - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx similarity index 77% rename from packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx index 95f7ef67a4..e2a6e61d39 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx @@ -1,15 +1,16 @@ +import { type ReactNode } from 'react'; import { NavigationMenuItemType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; -import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor'; import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; -import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; -import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; -import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; +import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular'; import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel'; -import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; +import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; @@ -17,57 +18,35 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState import { viewsSelector } from '@/views/states/selectors/viewsSelector'; import { ViewKey } from '@/views/types/ViewKey'; -type WorkspaceNavigationMenuItemFolderSubItemProps = { +type NavigationMenuItemFolderSubItemProps = { navigationMenuItem: NavigationMenuItem; index: number; arrayLength: number; selectedNavigationMenuItemIndex: number; - onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; - selectedNavigationMenuItemId: string | null; - isContextDragging: boolean; + isDragging: boolean; + rightOptions?: ReactNode; + onClick?: () => void; + onNavigationMenuItemClick?: (params: { + item: NavigationMenuItem; + objectMetadataItem?: ObjectMetadataItem; + }) => void; + selectedNavigationMenuItemId?: string | null; }; -export const WorkspaceNavigationMenuItemFolderSubItem = ({ +export const NavigationMenuItemFolderSubItem = ({ navigationMenuItem, index, arrayLength, selectedNavigationMenuItemIndex, + isDragging, + rightOptions, + onClick, onNavigationMenuItemClick, selectedNavigationMenuItemId, - isContextDragging, -}: WorkspaceNavigationMenuItemFolderSubItemProps) => { - const isLayoutCustomizationModeEnabled = useAtomStateValue( - isLayoutCustomizationModeEnabledState, - ); +}: NavigationMenuItemFolderSubItemProps) => { const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); - const objectMetadataItem = - navigationMenuItem.type === NavigationMenuItemType.OBJECT || - navigationMenuItem.type === NavigationMenuItemType.VIEW || - navigationMenuItem.type === NavigationMenuItemType.RECORD - ? getObjectMetadataForNavigationMenuItem( - navigationMenuItem, - objectMetadataItems, - views, - ) - : null; - - const isEditableInEditMode = - isLayoutCustomizationModeEnabled && - isDefined(onNavigationMenuItemClick) && - (navigationMenuItem.type === NavigationMenuItemType.LINK || - isDefined(objectMetadataItem)); - - const handleEditModeClick = - isEditableInEditMode && isDefined(onNavigationMenuItemClick) - ? () => - onNavigationMenuItemClick({ - item: navigationMenuItem, - objectMetadataItem: objectMetadataItem ?? undefined, - }) - : undefined; - const label = getNavigationMenuItemLabel( navigationMenuItem, objectMetadataItems, @@ -85,10 +64,36 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({ ); const view = isDefined(navigationMenuItem.viewId) - ? views.find((view) => view.id === navigationMenuItem.viewId) + ? views.find((viewItem) => viewItem.id === navigationMenuItem.viewId) : undefined; const isIndexView = view?.key === ViewKey.INDEX; + const objectMetadataItem = + navigationMenuItem.type === NavigationMenuItemType.OBJECT || + navigationMenuItem.type === NavigationMenuItemType.VIEW || + navigationMenuItem.type === NavigationMenuItemType.RECORD + ? getObjectMetadataForNavigationMenuItem( + navigationMenuItem, + objectMetadataItems, + views, + ) + : null; + + const isEditable = + isDefined(onNavigationMenuItemClick) && + (navigationMenuItem.type === NavigationMenuItemType.LINK || + isDefined(objectMetadataItem)); + + const handleClick = + onClick ?? + (isEditable + ? () => + onNavigationMenuItemClick({ + item: navigationMenuItem, + objectMetadataItem: objectMetadataItem ?? undefined, + }) + : undefined); + return ( ( )} - to={isContextDragging || handleEditModeClick ? undefined : computedLink} - onClick={handleEditModeClick} + iconColor={getEffectiveNavigationMenuItemColor(navigationMenuItem)} + to={isDragging || handleClick ? undefined : computedLink} + onClick={handleClick} active={index === selectedNavigationMenuItemIndex} isSelectedInEditMode={ selectedNavigationMenuItemId === navigationMenuItem.id @@ -114,7 +120,8 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({ arrayLength, selectedIndex: selectedNavigationMenuItemIndex, })} - isDragging={isContextDragging} + rightOptions={rightOptions} + isDragging={isDragging} triggerEvent="CLICK" /> ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolders.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolders.tsx deleted file mode 100644 index 86e023fb1f..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolders.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useState } from 'react'; -import { IconFolder } from 'twenty-ui/display'; - -import { FavoritesFolderItem } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesFolderItem'; -import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder'; -import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; -import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput'; - -export const NavigationMenuItemFolders = () => { - const [newFolderName, setNewFolderName] = useState(''); - - const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder(); - const { createNewNavigationMenuItemFolder } = - useCreateNavigationMenuItemFolder(); - - const [ - isNavigationMenuItemFolderCreating, - setIsNavigationMenuItemFolderCreating, - ] = useAtomState(isNavigationMenuItemFolderCreatingState); - - const handleNavigationMenuItemFolderNameChange = (value: string) => { - setNewFolderName(value); - }; - - const handleSubmitNavigationMenuItemFolderCreation = async ( - value: string, - ) => { - if (value === '') return; - - setIsNavigationMenuItemFolderCreating(false); - setNewFolderName(''); - await createNewNavigationMenuItemFolder(value); - return true; - }; - - const handleClickOutside = async ( - _event: MouseEvent | TouchEvent, - value: string, - ) => { - if (!value) { - setIsNavigationMenuItemFolderCreating(false); - return; - } - - setIsNavigationMenuItemFolderCreating(false); - setNewFolderName(''); - await createNewNavigationMenuItemFolder(value); - }; - - const handleCancelNavigationMenuItemFolderCreation = () => { - setNewFolderName(''); - setIsNavigationMenuItemFolderCreating(false); - }; - - return ( - <> - {isNavigationMenuItemFolderCreating && ( - - - - )} - {userNavigationMenuItemsByFolder.map((folder) => ( - 1} - /> - ))} - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx deleted file mode 100644 index cdab564c6c..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { styled } from '@linaria/react'; -import { useLingui } from '@lingui/react/macro'; -import React, { useContext, useState } from 'react'; -import { SidePanelPages } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { - IconChevronDown, - IconChevronRight, - IconPlus, - useIcons, -} from 'twenty-ui/display'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; - -import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection'; -import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage'; -import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState'; -import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState'; -import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; -import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -import { useIsMobile } from 'twenty-ui/utilities'; - -import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget'; -import { - FOLDER_HEADER_SLOT_COLLISION_PRIORITY, - WorkspaceDndKitDroppableSlot, -} from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitDroppableSlot'; -import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem'; -import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem'; -import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; -import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; -import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext'; -import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; -import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; -import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; -import { type NavigationMenuItem } from '~/generated-metadata/graphql'; -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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; - -const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>` - border: ${({ $isSelectedInEditMode }) => - $isSelectedInEditMode - ? `1px solid ${themeCssVariables.color.blue}` - : '1px solid transparent'}; - border-radius: ${themeCssVariables.border.radius.sm}; - transition: background-color 150ms ease-in-out; - - &[data-drag-over-header='true'] { - background-color: ${themeCssVariables.background.transparent.blue}; - } - - &[data-forbidden-drop-target='true'] { - background-color: ${themeCssVariables.background.transparent.danger}; - } -`; - -const StyledFolderDroppableContent = styled.div` - display: flex; - flex-direction: column; -`; - -const StyledFolderExpandableWrapper = styled.div` - & > div { - overflow: visible !important; - } -`; - -type WorkspaceNavigationMenuItemsFolderProps = { - folderId: string; - folderName: string; - folderIconKey?: string | null; - folderColor?: string | null; - navigationMenuItems: NavigationMenuItem[]; - isGroup: boolean; - isSelectedInEditMode?: boolean; - onEditModeClick?: () => void; - onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; - selectedNavigationMenuItemId?: string | null; - isDragging?: boolean; -}; - -export const WorkspaceNavigationMenuItemsFolder = ({ - folderId, - folderName, - folderIconKey, - folderColor, - navigationMenuItems, - isGroup, - isSelectedInEditMode = false, - onEditModeClick, - onNavigationMenuItemClick, - selectedNavigationMenuItemId = null, - isDragging = false, -}: WorkspaceNavigationMenuItemsFolderProps) => { - const isLayoutCustomizationModeEnabled = useAtomStateValue( - isLayoutCustomizationModeEnabledState, - ); - const { theme } = useContext(ThemeContext); - const { getIcon } = useIcons(); - const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); - const isMobile = useIsMobile(); - const { t } = useLingui(); - const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = - useWorkspaceFolderOpenState({ folderId, navigationMenuItems }); - const { openAddItemToFolderPage } = useOpenAddItemToFolderPage(); - const sidePanelPage = useAtomStateValue(sidePanelPageState); - const addMenuItemInsertionContext = useAtomStateValue( - addMenuItemInsertionContextState, - ); - - const folderContentLengthForTree = isLayoutCustomizationModeEnabled - ? navigationMenuItems.length + 1 - : navigationMenuItems.length; - - const handleAddMenuItemToFolder = () => { - openAddItemToFolderPage({ - targetFolderId: folderId, - targetIndex: navigationMenuItems.length, - resetNavigationStack: true, - }); - }; - - const shouldUseEditModeClick = - isLayoutCustomizationModeEnabled && isDefined(onEditModeClick); - const handleClick = shouldUseEditModeClick - ? (e?: React.MouseEvent) => { - e?.stopPropagation(); - if (isSelectedInEditMode) { - handleToggle(); - } else { - onEditModeClick?.(); - } - } - : handleToggle; - - const [skipInitialExpandAnimation] = useState(() => isOpen); - - const { isDragging: isContextDragging } = useContext( - NavigationMenuItemDragContext, - ); - const setSortableDropTargetRef = useContext(SortableDropTargetRefContext); - const folderContentDropDisabled = useIsDropDisabledForSection(true); - - const { activeDropTargetId, forbiddenDropTargetId } = useContext( - NavigationDropTargetContext, - ); - const folderHeaderDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`; - const folderContentDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`; - const folderHeaderSlotId = getDndKitDropTargetId(folderHeaderDroppableId, 0); - const isForbiddenDropTarget = - isDefined(forbiddenDropTargetId) && - (forbiddenDropTargetId.startsWith(`${folderContentDroppableId}::`) || - forbiddenDropTargetId.startsWith(`${folderHeaderDroppableId}::`)); - const isDragOverFolderHeader = - !isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId; - const isCompact = - isLayoutCustomizationModeEnabled || navigationMenuItems.length === 0; - - const headerItem = ( - = 0} - onClick={handleClick} - className="navigation-drawer-item" - triggerEvent="CLICK" - preventCollapseOnMobile={isMobile} - isDragging={isDragging} - alwaysShowRightOptions - rightOptions={ -
{ - e.stopPropagation(); - handleToggle(); - }} - > - {isOpen ? ( - - ) : ( - - )} -
- } - /> - ); - - return ( - - -
- - {headerItem} - -
- - - - {navigationMenuItems.map((navigationMenuItem, index) => ( - - - - - - - ))} - - - {isLayoutCustomizationModeEnabled && ( - - )} - - - - -
-
- ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts similarity index 95% rename from packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts index 7dfc3b99e7..075c23dccd 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts @@ -15,15 +15,15 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -type UseWorkspaceFolderOpenStateParams = { +type UseNavigationMenuItemFolderOpenStateParams = { folderId: string; navigationMenuItems: NavigationMenuItem[]; }; -export const useWorkspaceFolderOpenState = ({ +export const useNavigationMenuItemFolderOpenState = ({ folderId, navigationMenuItems, -}: UseWorkspaceFolderOpenStateParams) => { +}: UseNavigationMenuItemFolderOpenStateParams) => { const location = useLocation(); const navigate = useNavigate(); const currentPath = location.pathname; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceSectionItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts similarity index 97% rename from packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceSectionItems.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts index b13a7651ee..7ceda31038 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceSectionItems.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts @@ -21,7 +21,7 @@ export type NavigationMenuItemClickParams = { objectMetadataItem?: ObjectMetadataItem | null; }; -export const useWorkspaceSectionItems = (): NavigationMenuItem[] => { +export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { const { workspaceNavigationMenuItems } = useNavigationMenuItemsData(); const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems(); const { workspaceNavigationMenuItemsByFolder } = diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx index 7a9aca9a45..97c5f60040 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx @@ -1,19 +1,21 @@ +import { IconArrowUpRight } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; import { getLinkNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemComputedLink'; import { getLinkNavigationMenuItemLabel } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemLabel'; -import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps'; +import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { IconArrowUpRight } from 'twenty-ui/display'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -type NavigationMenuItemLinkDisplayProps = WorkspaceSectionItemContentProps; +type NavigationMenuItemLinkDisplayProps = NavigationMenuItemSectionContentProps; export const NavigationMenuItemLinkDisplay = ({ item, editModeProps, isDragging, + rightOptions, }: NavigationMenuItemLinkDisplayProps) => { const isLayoutCustomizationModeEnabled = useAtomStateValue( isLayoutCustomizationModeEnabledState, @@ -22,6 +24,14 @@ export const NavigationMenuItemLinkDisplay = ({ const label = getLinkNavigationMenuItemLabel(item); const computedLink = getLinkNavigationMenuItemComputedLink(item); + const defaultRightOptions = !isLayoutCustomizationModeEnabled && ( + + ); + return ( } active={false} - isSelectedInEditMode={editModeProps.isSelectedInEditMode} + isSelectedInEditMode={editModeProps?.isSelectedInEditMode} isDragging={isDragging} triggerEvent="CLICK" - rightOptions={ - !isLayoutCustomizationModeEnabled && ( - - ) - } + rightOptions={rightOptions ?? defaultRightOptions} /> ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx index 8fac10513b..2be2eda036 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -1,3 +1,5 @@ +import type { ReactNode } from 'react'; + import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; import { useObjectNavItemColor } from '@/navigation-menu-item/common/hooks/useObjectNavItemColor'; @@ -28,6 +30,7 @@ export type NavigationDrawerItemForObjectMetadataItemProps = { onEditModeClick?: () => void; onActiveItemClickWhenNotInEditMode?: () => void; isDragging?: boolean; + rightOptions?: ReactNode; }; export const NavigationDrawerItemForObjectMetadataItem = ({ @@ -37,6 +40,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ onEditModeClick, onActiveItemClickWhenNotInEditMode: _onActiveItemClickWhenNotInEditMode, isDragging = false, + rightOptions, }: NavigationDrawerItemForObjectMetadataItemProps) => { const isLayoutCustomizationModeEnabled = useAtomStateValue( isLayoutCustomizationModeEnabledState, @@ -171,6 +175,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ isSelectedInEditMode={isSelectedInEditMode} isDragging={isDragging} triggerEvent={isLayoutCustomizationModeEnabled ? 'CLICK' : undefined} + rightOptions={rightOptions} /> ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay.tsx index 1dbbe1ae08..ce0e2b929b 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay.tsx @@ -1,16 +1,18 @@ import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; import { NavigationDrawerItemForObjectMetadataItem } from '@/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem'; -import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps'; +import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -type NavigationMenuItemObjectDisplayProps = WorkspaceSectionItemContentProps; +type NavigationMenuItemObjectDisplayProps = + NavigationMenuItemSectionContentProps; export const NavigationMenuItemObjectDisplay = ({ item, editModeProps, isDragging, + rightOptions, onActiveObjectMetadataItemClick, }: NavigationMenuItemObjectDisplayProps) => { const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); @@ -27,9 +29,10 @@ export const NavigationMenuItemObjectDisplay = ({ onActiveObjectMetadataItemClick(objectMetadataItem, item.id) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/WorkspaceOrphanDropTarget.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx similarity index 62% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/WorkspaceOrphanDropTarget.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx index 58a8d50721..520f3d5986 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/WorkspaceOrphanDropTarget.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx @@ -1,30 +1,31 @@ import type { ReactNode } from 'react'; -import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget'; -import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; +import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget'; -type WorkspaceOrphanDropTargetProps = { +type NavigationMenuItemOrphanDropTargetProps = { index: number; compact?: boolean; children?: ReactNode; + sectionId?: NavigationSections; + droppableId?: string; }; -export const WorkspaceOrphanDropTarget = ({ +export const NavigationMenuItemOrphanDropTarget = ({ index, compact = false, children, -}: WorkspaceOrphanDropTargetProps) => ( + sectionId = NavigationSections.WORKSPACE, + droppableId = NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, +}: NavigationMenuItemOrphanDropTargetProps) => ( {children} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemSection.tsx new file mode 100644 index 0000000000..f8cc080b49 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemSection.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from 'react'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; + +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; +import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; + +type NavigationMenuItemSectionProps = { + title: string; + isOpen: boolean; + onToggle: () => void; + rightIcon?: ReactNode; + alwaysShowRightIcon?: boolean; + forceExpanded?: boolean; + children: ReactNode; + contentWrapper?: (children: ReactNode) => ReactNode; +}; + +export const NavigationMenuItemSection = ({ + title, + isOpen, + onToggle, + rightIcon, + alwaysShowRightIcon, + forceExpanded = false, + children, + contentWrapper, +}: NavigationMenuItemSectionProps) => { + const content = ( + + {children} + + ); + + return ( + + + + + {contentWrapper ? contentWrapper(content) : content} + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesFolderItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesFolderItem.tsx deleted file mode 100644 index b356de125e..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesFolderItem.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import { useLingui } from '@lingui/react/macro'; -import { useContext, useState } from 'react'; -import { type NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; -import { createPortal } from 'react-dom'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display'; - -import { LightIconButton } from 'twenty-ui/input'; -import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -import { useIsMobile } from 'twenty-ui/utilities'; - -import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget'; -import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem'; -import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderNavigationDrawerItemDropdown'; -import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; -import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; -import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/common/constants/NavigationMenuItemFolderDeleteModalId'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; -import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder'; -import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder'; -import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; -import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor'; -import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; -import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; -import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular'; -import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel'; -import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; -import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; -import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; -import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; -import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { useModal } from '@/ui/layout/modal/hooks/useModal'; -import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; -import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput'; -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 { currentNavigationMenuItemFolderIdState } from '@/navigation-menu-item/common/states/currentNavigationMenuItemFolderIdState'; -import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -import { ViewKey } from '@/views/types/ViewKey'; -import { isNonEmptyString } from '@sniptt/guards'; - -type FavoritesFolderItemProps = { - folder: { - id: string; - folderName: string; - navigationMenuItems: NavigationMenuItem[]; - }; - isGroup: boolean; - isWorkspaceFolder?: boolean; -}; - -export const FavoritesFolderItem = ({ - folder, - isGroup, - isWorkspaceFolder = false, -}: FavoritesFolderItemProps) => { - const { t } = useLingui(); - const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - const views = useAtomStateValue(viewsSelector); - const location = useLocation(); - const navigate = useNavigate(); - const currentPath = location.pathname; - const currentViewPath = location.pathname + location.search; - const { isDragging } = useContext(NavigationMenuItemDragContext); - const [ - isNavigationMenuItemFolderRenaming, - setIsNavigationMenuItemFolderRenaming, - ] = useState(false); - const [navigationMenuItemFolderName, setNavigationMenuItemFolderName] = - useState(folder.folderName); - const { openModal } = useModal(); - - const isMobile = useIsMobile(); - - const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] = - useAtomState(openNavigationMenuItemFolderIdsState); - - const setCurrentNavigationMenuItemFolderId = useSetAtomState( - currentNavigationMenuItemFolderIdState, - ); - - const isOpen = openNavigationMenuItemFolderIds.includes(folder.id); - - const handleToggle = () => { - if (isMobile) { - setCurrentNavigationMenuItemFolderId((prev) => - prev === folder.id ? null : folder.id, - ); - } else { - setOpenNavigationMenuItemFolderIds((currentOpenFolders) => { - if (isOpen) { - return currentOpenFolders.filter((id) => id !== folder.id); - } else { - return [...currentOpenFolders, folder.id]; - } - }); - } - - if (!isOpen) { - const firstNonLinkItem = folder.navigationMenuItems.find((item) => { - if (item.type === NavigationMenuItemType.LINK) { - return false; - } - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - return isNonEmptyString(computedLink); - }); - if (isDefined(firstNonLinkItem)) { - const link = getNavigationMenuItemComputedLink( - firstNonLinkItem, - objectMetadataItems, - views, - ); - if (isNonEmptyString(link)) { - navigate(link); - } - } - } - }; - - const { renameNavigationMenuItemFolder } = - useRenameNavigationMenuItemFolder(); - const { deleteNavigationMenuItemFolder } = - useDeleteNavigationMenuItemFolder(); - - const dropdownId = `navigation-menu-item-folder-edit-${folder.id}`; - - const isDropdownOpen = useAtomComponentStateValue( - isDropdownOpenComponentState, - dropdownId, - ); - - const { closeDropdown } = useCloseDropdown(); - - const selectedNavigationMenuItemIndex = folder.navigationMenuItems.findIndex( - (item) => { - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - return isLocationMatchingNavigationMenuItem( - currentPath, - currentViewPath, - item.type, - computedLink, - ); - }, - ); - - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); - - const navigationMenuItemFolderContentLength = - folder.navigationMenuItems.length; - - const handleSubmitRename = async (value: string) => { - if (value === '') return; - await renameNavigationMenuItemFolder(folder.id, value); - setIsNavigationMenuItemFolderRenaming(false); - return true; - }; - - const handleCancelRename = () => { - setNavigationMenuItemFolderName(folder.folderName); - setIsNavigationMenuItemFolderRenaming(false); - }; - - const handleClickOutside = async ( - _event: MouseEvent | TouchEvent, - value: string, - ) => { - if (!value) { - setIsNavigationMenuItemFolderRenaming(false); - return; - } - - await renameNavigationMenuItemFolder(folder.id, value); - setIsNavigationMenuItemFolderRenaming(false); - }; - - const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.id}`; - - const handleNavigationMenuItemFolderDelete = async () => { - if (folder.navigationMenuItems.length > 0) { - openModal(modalId); - closeDropdown(dropdownId); - } else { - await deleteNavigationMenuItemFolder(folder.id); - closeDropdown(dropdownId); - } - }; - - const handleConfirmDelete = async () => { - await deleteNavigationMenuItemFolder(folder.id); - }; - - const rightOptions = isWorkspaceFolder ? undefined : ( - setIsNavigationMenuItemFolderRenaming(true)} - onDelete={handleNavigationMenuItemFolderDelete} - closeDropdown={() => { - closeDropdown(dropdownId); - }} - /> - ); - - const isModalOpened = useAtomComponentStateValue( - isModalOpenedComponentState, - modalId, - ); - - const navigationMenuItemCount = folder.navigationMenuItems.length; - - const folderDroppableId = `folder-${folder.id}`; - - return ( - <> - - {isNavigationMenuItemFolderRenaming ? ( - - ) : ( - - - - )} - - - {folder.navigationMenuItems.map((navigationMenuItem, index) => { - const label = getNavigationMenuItemLabel( - navigationMenuItem, - objectMetadataItems, - views, - ); - const computedLink = getNavigationMenuItemComputedLink( - navigationMenuItem, - objectMetadataItems, - views, - ); - const objectNameSingular = getNavigationMenuItemObjectNameSingular( - navigationMenuItem, - objectMetadataItems, - views, - ); - const view = isDefined(navigationMenuItem.viewId) - ? views.find((view) => view.id === navigationMenuItem.viewId) - : undefined; - const isIndexView = view?.key === ViewKey.INDEX; - - return ( - - ( - - )} - to={isDragging ? undefined : computedLink} - active={index === selectedNavigationMenuItemIndex} - subItemState={getNavigationSubItemLeftAdornment({ - index, - arrayLength: navigationMenuItemFolderContentLength, - selectedIndex: selectedNavigationMenuItemIndex, - })} - rightOptions={ - isWorkspaceFolder ? undefined : ( - { - e.stopPropagation(); - deleteNavigationMenuItem(navigationMenuItem.id); - }} - accent="tertiary" - /> - ) - } - isDragging={isDragging} - triggerEvent="CLICK" - iconColor={getEffectiveNavigationMenuItemColor( - navigationMenuItem, - )} - /> - - ); - })} - - - - {isModalOpened && - createPortal( - 1 - ? t`Remove ${navigationMenuItemCount} navigation menu items?` - : t`Remove ${navigationMenuItemCount} navigation menu item?` - } - subtitle={ - folder.navigationMenuItems.length > 1 - ? t`This action will delete this folder and all ${navigationMenuItemCount} navigation menu items inside. Do you want to continue?` - : t`This action will delete this folder and the navigation menu item inside. Do you want to continue?` - } - onConfirmClick={handleConfirmDelete} - confirmButtonText={t`Delete Folder`} - />, - document.body, - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesOrphanItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesOrphanItems.tsx deleted file mode 100644 index 03708b55f3..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesOrphanItems.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { styled } from '@linaria/react'; -import { useContext } from 'react'; -import { useLocation } from 'react-router-dom'; -import { IconHeartOff } from 'twenty-ui/display'; -import { LightIconButton } from 'twenty-ui/input'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; - -import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem'; -import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; -import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; -import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor'; -import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; -import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; -import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular'; -import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel'; -import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; -import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; - -const StyledEmptyContainer = styled.div` - width: 100%; -`; - -const StyledOrphanNavigationMenuItemsContainer = styled.div` - margin-bottom: ${themeCssVariables.betweenSiblingsGap}; -`; - -export const FavoritesOrphanItems = () => { - const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - const views = useAtomStateValue(viewsSelector); - const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); - const currentPath = useLocation().pathname; - const currentViewPath = useLocation().pathname + useLocation().search; - const { isDragging } = useContext(NavigationMenuItemDragContext); - - const orphanNavigationMenuItems = navigationMenuItemsSorted.filter( - (item) => !item.folderId, - ); - - return orphanNavigationMenuItems.length > 0 ? ( - <> - {orphanNavigationMenuItems.map((navigationMenuItem, index) => { - const label = getNavigationMenuItemLabel( - navigationMenuItem, - objectMetadataItems, - views, - ); - const computedLink = getNavigationMenuItemComputedLink( - navigationMenuItem, - objectMetadataItems, - views, - ); - const objectNameSingular = getNavigationMenuItemObjectNameSingular( - navigationMenuItem, - objectMetadataItems, - views, - ); - - return ( - - - ( - - )} - iconColor={getEffectiveNavigationMenuItemColor( - navigationMenuItem, - )} - active={isLocationMatchingNavigationMenuItem( - currentPath, - currentViewPath, - navigationMenuItem.type, - computedLink, - )} - to={isDragging ? undefined : computedLink} - rightOptions={ - { - e.stopPropagation(); - deleteNavigationMenuItem(navigationMenuItem.id); - }} - accent="tertiary" - /> - } - isDragging={isDragging} - triggerEvent="CLICK" - /> - - - ); - })} - - ) : ( - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx index 976072c04c..256857b5bc 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx @@ -1,23 +1,63 @@ +import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { IconFolderPlus } from 'twenty-ui/display'; +import { useCallback, useContext, useMemo, useState } from 'react'; +import { IconFolder, IconFolderPlus, IconHeartOff } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; -import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -import { FavoritesOrphanItems } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesOrphanItems'; -import { NavigationMenuItemFolders } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolders'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; +import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; +import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay'; +import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot'; +import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; +import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection'; +import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder'; import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; -import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget'; +import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; -import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; +import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; + +const StyledList = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.betweenSiblingsGap}; + padding-top: ${themeCssVariables.betweenSiblingsGap}; +`; + +const StyledListItemRow = styled.div` + display: flex; + flex-direction: column; + gap: 0; +`; + +const StyledEmptyContainer = styled.div` + width: 100%; +`; + +const ORPHAN_DROPPABLE_ID = + NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS; export const FavoritesSection = () => { const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder(); + const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { isDragging } = useContext(NavigationMenuItemDragContext); + const favoritesDropDisabled = useIsDropDisabledForSection(false); + + const [newFolderName, setNewFolderName] = useState(''); + const { createNewNavigationMenuItemFolder } = + useCreateNavigationMenuItemFolder(); const [ isNavigationMenuItemFolderCreating, @@ -33,13 +73,71 @@ export const FavoritesSection = () => { 'Favorites', ); + const topLevelItems = useMemo( + () => navigationMenuItemsSorted.filter((item) => !item.folderId), + [navigationMenuItemsSorted], + ); + + const folderChildrenById = useMemo(() => { + const map = new Map(); + for (const folder of userNavigationMenuItemsByFolder) { + map.set(folder.id, folder.navigationMenuItems); + } + return map; + }, [userNavigationMenuItemsByFolder]); + + const folderCount = useMemo( + () => topLevelItems.filter(isNavigationMenuItemFolder).length, + [topLevelItems], + ); + const toggleNewFolder = () => { openNavigationSection(); setIsNavigationMenuItemFolderCreating((current) => !current); }; + const handleSubmitFolderCreation = async (value: string) => { + if (value === '') return; + setIsNavigationMenuItemFolderCreating(false); + setNewFolderName(''); + await createNewNavigationMenuItemFolder(value); + return true; + }; + + const handleClickOutside = async ( + _event: MouseEvent | TouchEvent, + value: string, + ) => { + if (!value) { + setIsNavigationMenuItemFolderCreating(false); + return; + } + setIsNavigationMenuItemFolderCreating(false); + setNewFolderName(''); + await createNewNavigationMenuItemFolder(value); + }; + + const handleCancelFolderCreation = () => { + setNewFolderName(''); + setIsNavigationMenuItemFolderCreating(false); + }; + + const makeRightOptions = useCallback( + (item: NavigationMenuItem) => ( + { + event.stopPropagation(); + deleteNavigationMenuItem(item.id); + }} + accent="tertiary" + /> + ), + [deleteNavigationMenuItem], + ); + if ( - navigationMenuItemsSorted.length === 0 && + topLevelItems.length === 0 && !isNavigationMenuItemFolderCreating && userNavigationMenuItemsByFolder.length === 0 ) { @@ -47,31 +145,92 @@ export const FavoritesSection = () => { } return ( - - - - } - isOpen={isNavigationSectionOpen} + - - - - - - + } + > + {isNavigationMenuItemFolderCreating && ( + + + + )} + {topLevelItems.length > 0 ? ( + + {topLevelItems.map((item, index) => ( + + {index === 0 ? ( + + + + ) : ( + + )} + + + + + ))} + + + + + ) : ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx index 633be13d70..f23857584d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx @@ -1,6 +1,7 @@ import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; +import { NavigationMenuItemDndKitProvider } from '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider'; import { FavoritesSection } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesSection'; -import { FavoritesDndKitProvider } from '@/navigation/components/FavoritesDndKitProvider'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { AnimatedEaseInOut } from 'twenty-ui/utilities'; @@ -11,9 +12,9 @@ export const FavoritesSectionDispatcher = () => { return ( - + - + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts similarity index 69% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts index beb490461d..3f045d6d82 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts @@ -1,16 +1,20 @@ -import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import type { ReactNode } from 'react'; + +import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; -export type WorkspaceSectionItemContentProps = { +export type NavigationMenuItemSectionContentProps = { item: NavigationMenuItem; - editModeProps: EditModeProps; + isEditInPlace?: boolean; + editModeProps?: EditModeProps; isDragging: boolean; folderChildrenById: Map; folderCount: number; - selectedNavigationMenuItemId: string | null; + rightOptions?: ReactNode; + selectedNavigationMenuItemId?: string | null; onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; onActiveObjectMetadataItemClick?: ( objectMetadataItem: ObjectMetadataItem, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps.ts similarity index 85% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps.ts rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps.ts index 673ab21149..40413c6374 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps.ts @@ -1,10 +1,10 @@ -import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; -export type WorkspaceSectionListDndKitProps = { +export type NavigationMenuItemSectionListDndKitProps = { filteredItems: NavigationMenuItem[]; getEditModeProps: (item: NavigationMenuItem) => EditModeProps; folderChildrenById: Map; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItemsDispatcher.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItemsDispatcher.tsx deleted file mode 100644 index f24e8c7fdf..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItemsDispatcher.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems'; - -export const WorkspaceNavigationMenuItemsDispatcher = () => { - return ; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx similarity index 92% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx index 447658a085..c0dea72b36 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx @@ -20,14 +20,14 @@ import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edi import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; import { type NavigationMenuItemClickParams, - useWorkspaceSectionItems, -} from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; + useNavigationMenuItemSectionItems, +} from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; -import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit'; -import { NavigationDrawerSectionForWorkspaceItems } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems'; +import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit'; +import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; @@ -42,8 +42,8 @@ const StyledRightIconsContainer = styled.div` gap: ${themeCssVariables.spacing[1]}; `; -export const WorkspaceNavigationMenuItems = () => { - const items = useWorkspaceSectionItems(); +export const WorkspaceSection = () => { + const items = useNavigationMenuItemSectionItems(); const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); @@ -173,7 +173,7 @@ export const WorkspaceNavigationMenuItems = () => { }; return ( - { onClick={handleAddMenuItem} /> ) : ( -
+
import( - '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit' + '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit' ).then((m) => ({ default: m.WorkspaceSectionListDndKit })), ); @@ -35,7 +32,7 @@ const StyledWorkspaceSectionContentGapOffset = styled.div` margin-top: calc(-1 * ${themeCssVariables.betweenSiblingsGap}); `; -type NavigationDrawerSectionForWorkspaceItemsProps = { +type WorkspaceSectionContainerProps = { sectionTitle: string; items: NavigationMenuItem[]; rightIcon?: React.ReactNode; @@ -47,14 +44,14 @@ type NavigationDrawerSectionForWorkspaceItemsProps = { ) => void; }; -export const NavigationDrawerSectionForWorkspaceItems = ({ +export const WorkspaceSectionContainer = ({ sectionTitle, items, rightIcon, selectedNavigationMenuItemId = null, onNavigationMenuItemClick, onActiveObjectMetadataItemClick, -}: NavigationDrawerSectionForWorkspaceItemsProps) => { +}: WorkspaceSectionContainerProps) => { const isLayoutCustomizationModeEnabled = useAtomStateValue( isLayoutCustomizationModeEnabledState, ); @@ -145,58 +142,45 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ } return ( - - - toggleNavigationSection()} - rightIcon={rightIcon} - alwaysShowRightIcon={isLayoutCustomizationModeEnabled} - isOpen={isNavigationSectionOpen} - /> - - - - {isLayoutCustomizationModeEnabled ? ( - - } - > - - - ) : ( - toggleNavigationSection()} + rightIcon={rightIcon} + alwaysShowRightIcon={isLayoutCustomizationModeEnabled} + forceExpanded={isAddToNavigationDropTargetVisible} + contentWrapper={(children) => ( + + {children} + + )} + > + {isLayoutCustomizationModeEnabled ? ( + - )} - - - + } + > + + + ) : ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher.tsx new file mode 100644 index 0000000000..5d256a1b3f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher.tsx @@ -0,0 +1,5 @@ +import { WorkspaceSection } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSection'; + +export const WorkspaceSectionDispatcher = () => { + return ; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx similarity index 78% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx index 054a7bbe04..619a654b45 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx @@ -1,6 +1,6 @@ +import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot'; +import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; -import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitDroppableSlot'; -import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; import { NavigationMenuItemType } from 'twenty-shared/types'; import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; @@ -12,9 +12,11 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay'; -import { WorkspaceOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/WorkspaceOrphanDropTarget'; +import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget'; import { WorkspaceSectionAddMenuItemButton } from '@/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton'; -import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps'; +import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps'; + +type WorkspaceSectionListDndKitProps = NavigationMenuItemSectionListDndKitProps; const StyledList = styled.div` display: flex; @@ -53,8 +55,8 @@ export const WorkspaceSectionListDndKit = ({ {filteredItems.map((item, index) => ( - - + - + ))} - - {isAddMenuItemButtonVisible && } - - + + {addToNavigationFallbackDestination?.droppableId === NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS && addToNavigationFallbackDestination.index > filteredItems.length && ( - - - + )} ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly.tsx similarity index 79% rename from packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx rename to packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly.tsx index 8ed3dfb9d2..55dff36520 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly.tsx @@ -3,7 +3,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; import { NavigationMenuItemType } from 'twenty-shared/types'; -import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps'; +import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps'; import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay'; const StyledList = styled.div` @@ -13,8 +13,8 @@ const StyledList = styled.div` padding-top: ${themeCssVariables.betweenSiblingsGap}; `; -type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick< - WorkspaceSectionListDndKitProps, +type WorkspaceSectionListReadOnlyProps = Pick< + NavigationMenuItemSectionListDndKitProps, 'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick' >; @@ -23,11 +23,11 @@ const READ_ONLY_EDIT_MODE_PROPS = { onEditModeClick: undefined, } as const; -export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({ +export const WorkspaceSectionListReadOnly = ({ filteredItems, folderChildrenById, onActiveObjectMetadataItemClick, -}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => { +}: WorkspaceSectionListReadOnlyProps) => { const folderCount = filteredItems.filter( (item) => item.type === NavigationMenuItemType.FOLDER, ).length; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit.ts new file mode 100644 index 0000000000..be010c9b87 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit.ts @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/common/constants/NavigationMenuItemFolderDeleteModalId'; +import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder'; +import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; + +type UseFavoritesFolderEditParams = { + folderId: string; + folderName: string; + navigationMenuItems: NavigationMenuItem[]; +}; + +export const useFavoritesFolderEdit = ({ + folderId, + folderName: initialFolderName, + navigationMenuItems, +}: UseFavoritesFolderEditParams) => { + const [isRenaming, setIsRenaming] = useState(false); + const [folderNameValue, setFolderNameValue] = useState(initialFolderName); + + const { renameNavigationMenuItemFolder } = + useRenameNavigationMenuItemFolder(); + const { deleteNavigationMenuItemFolder } = + useDeleteNavigationMenuItemFolder(); + const { openModal } = useModal(); + + const dropdownId = `navigation-menu-item-folder-edit-${folderId}`; + const isDropdownOpen = useAtomComponentStateValue( + isDropdownOpenComponentState, + dropdownId, + ); + const { closeDropdown } = useCloseDropdown(); + + const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folderId}`; + const isModalOpened = useAtomComponentStateValue( + isModalOpenedComponentState, + modalId, + ); + + const handleSubmitRename = async (value: string) => { + if (value === '') return; + await renameNavigationMenuItemFolder(folderId, value); + setIsRenaming(false); + return true; + }; + + const handleCancelRename = () => { + setFolderNameValue(initialFolderName); + setIsRenaming(false); + }; + + const handleClickOutsideRename = async ( + _event: MouseEvent | TouchEvent, + value: string, + ) => { + if (!value) { + setIsRenaming(false); + return; + } + await renameNavigationMenuItemFolder(folderId, value); + setIsRenaming(false); + }; + + const handleFolderDelete = async () => { + if (navigationMenuItems.length > 0) { + openModal(modalId); + closeDropdown(dropdownId); + } else { + await deleteNavigationMenuItemFolder(folderId); + closeDropdown(dropdownId); + } + }; + + const handleConfirmDelete = async () => { + await deleteNavigationMenuItemFolder(folderId); + }; + + return { + isRenaming, + setIsRenaming, + folderNameValue, + setFolderNameValue, + handleSubmitRename, + handleCancelRename, + handleClickOutsideRename, + handleFolderDelete, + handleConfirmDelete, + isDropdownOpen, + dropdownId, + closeDropdown: () => closeDropdown(dropdownId), + modalId, + isModalOpened, + navigationMenuItemCount: navigationMenuItems.length, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts index cc3deb3a33..66154b0da7 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts @@ -5,13 +5,13 @@ import { getPositionBetween } from '@/navigation-menu-item/common/utils/getPosit import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; export const useNavigationMenuItemMoveRemove = () => { const setNavigationMenuItemsDraft = useSetAtomState( navigationMenuItemsDraftState, ); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); const visibleItemIds = new Set(items.map((item) => item.id)); const moveUp = (navigationMenuItemId: string) => { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts index 4b0e98c37f..08dc2ad40d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts @@ -1,4 +1,4 @@ -import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -6,7 +6,7 @@ export const useSelectedNavigationMenuItemEditItem = () => { const selectedNavigationMenuItemInEditMode = useAtomStateValue( selectedNavigationMenuItemInEditModeState, ); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); const selectedItem = selectedNavigationMenuItemInEditMode ? items.find((item) => item.id === selectedNavigationMenuItemInEditMode) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts index bdd02111e5..717344f571 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts @@ -7,7 +7,7 @@ import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions'; import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove'; import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState'; import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/common/types/AddMenuItemInsertionContext'; @@ -60,7 +60,7 @@ export const useNavigationMenuItemEditOrganizeActions = addMenuItemInsertionContextState, ); const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove(); const selectedItem = selectedNavigationMenuItemInEditMode diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx index 411957be08..97cc19f279 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx @@ -16,11 +16,11 @@ const FavoritesSectionDispatcher = lazy(() => })), ); -const WorkspaceNavigationMenuItemsDispatcher = lazy(() => +const WorkspaceSectionDispatcher = lazy(() => import( - '@/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItemsDispatcher' + '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher' ).then((module) => ({ - default: module.WorkspaceNavigationMenuItemsDispatcher, + default: module.WorkspaceSectionDispatcher, })), ); @@ -36,7 +36,7 @@ export const MainNavigationDrawerScrollableItems = () => { }> - + diff --git a/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx b/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx deleted file mode 100644 index ab8208b796..0000000000 --- a/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { lazy, Suspense, useState, type ReactNode } from 'react'; - -import { PageDragDropProviderMountEffect } from '@/navigation/components/PageDragDropProviderMountEffect'; - -const LazyWorkspaceDndKitProvider = lazy(() => - import('@/navigation/components/WorkspaceDndKitProvider').then((m) => ({ - default: m.WorkspaceDndKitProvider, - })), -); - -type PageDragDropProviderProps = { - children: ReactNode; -}; - -export const PageDragDropProvider = ({ - children, -}: PageDragDropProviderProps) => { - const [hasProviderMounted, setHasProviderMounted] = useState(false); - - if (!hasProviderMounted) { - return ( - <> - setHasProviderMounted(true)} - /> - {children} - - ); - } - - return ( - {children}}> - {children} - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx b/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx deleted file mode 100644 index 4268852e67..0000000000 --- a/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { PointerActivationConstraints } from '@dnd-kit/dom'; -import { - DragDropProvider, - KeyboardSensor, - PointerSensor, -} from '@dnd-kit/react'; -import type { ReactNode } from 'react'; - -import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext'; -import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; - -import { useWorkspaceDndKit } from '@/navigation/hooks/useWorkspaceDndKit'; - -const WORKSPACE_DND_SENSORS = [ - PointerSensor.configure({ - activationConstraints: [ - new PointerActivationConstraints.Distance({ value: 8 }), - ], - }), - KeyboardSensor, -]; - -type WorkspaceDndKitProviderProps = { - children: ReactNode; -}; - -export const WorkspaceDndKitProvider = ({ - children, -}: WorkspaceDndKitProviderProps) => { - const { contextValues, handlers } = useWorkspaceDndKit(); - - return ( - - - - - sensors={WORKSPACE_DND_SENSORS} - onDragStart={handlers.onDragStart} - onDragOver={handlers.onDragOver} - onDragEnd={handlers.onDragEnd} - > - {children} - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useFavoritesDndKit.ts b/packages/twenty-front/src/modules/navigation/hooks/useFavoritesDndKit.ts deleted file mode 100644 index 7bc69094bb..0000000000 --- a/packages/twenty-front/src/modules/navigation/hooks/useFavoritesDndKit.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { type DragDropProvider } from '@dnd-kit/react'; -import { isSortable } from '@dnd-kit/react/sortable'; -import type { ResponderProvided } from '@hello-pangea/dnd'; -import { type ComponentProps, useCallback, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { type NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop'; -import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop'; -import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; -import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; - -import { DROP_RESULT_OPTIONS } from '@/navigation/constants/workspaceDndKitDropResultOptions'; -import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; -import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; -import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult'; - -type DragStartPayload = Parameters< - NonNullable< - ComponentProps>['onDragStart'] - > ->[0]; -type DragOverPayload = Parameters< - NonNullable< - ComponentProps>['onDragOver'] - > ->[0]; -type DragEndPayload = Parameters< - NonNullable< - ComponentProps>['onDragEnd'] - > ->[0]; - -export type FavoritesDndKitContextValues = { - dragSource: { sourceDroppableId: string | null }; - drag: { isDragging: boolean }; - dropTarget: { - activeDropTargetId: string | null; - setActiveDropTargetId: (id: string | null) => void; - forbiddenDropTargetId: string | null; - setForbiddenDropTargetId: (id: string | null) => void; - addToNavigationFallbackDestination: DropDestination | null; - }; -}; - -const resolveFavoritesDropTarget = ( - target: { - id?: unknown; - group?: unknown; - index?: unknown; - data?: unknown; - } | null, - getNavItemById: (id: string | undefined) => NavigationMenuItem | undefined, -): { - destination: DropDestination; - effectiveDropTargetId: string; - isTargetFolder: boolean; -} | null => { - if (target === null || target === undefined) { - return null; - } - - if (isDefined(target.group) && isDefined(target.index)) { - const group = String(target.group); - const index = Number(target.index); - if (!Number.isInteger(index) || index < 0) { - return null; - } - - const targetItem = getNavItemById( - target.id != null ? String(target.id) : undefined, - ); - const isTargetFolder = - isDefined(targetItem) && isNavigationMenuItemFolder(targetItem); - - const destination: DropDestination = isTargetFolder - ? { droppableId: `folder-header-${target.id}`, index: 0 } - : { droppableId: group, index }; - - const effectiveDropTargetId = isTargetFolder - ? getDndKitDropTargetId(`folder-header-${target.id}`, 0) - : getDndKitDropTargetId(group, index); - - return { destination, effectiveDropTargetId, isTargetFolder }; - } - - return null; -}; - -export const useFavoritesDndKit = (): { - contextValues: FavoritesDndKitContextValues; - handlers: { - onDragStart: (event: DragStartPayload) => void; - onDragOver: (event: DragOverPayload) => void; - onDragEnd: (event: DragEndPayload) => void; - }; -} => { - const [isDragging, setIsDragging] = useState(false); - const [sourceDroppableId, setSourceDroppableId] = useState( - null, - ); - const [activeDropTargetId, setActiveDropTargetId] = useState( - null, - ); - const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< - string | null - >(null); - const [ - addToNavigationFallbackDestination, - setAddToNavigationFallbackDestination, - ] = useState(null); - - const { navigationMenuItems } = useNavigationMenuItemsData(); - const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); - const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop(); - const { handleNavigationMenuItemDragAndDrop } = - useHandleNavigationMenuItemDragAndDrop(); - - const orphanItemCount = navigationMenuItemsSorted.filter( - (item: { folderId?: string | null }) => !isDefined(item.folderId), - ).length; - - const getNavItemById = useCallback( - (id: string | undefined) => - id ? navigationMenuItems.find((item) => item.id === id) : undefined, - [navigationMenuItems], - ); - - const handleDragStart = (event: DragStartPayload) => { - const { operation } = event; - setIsDragging(true); - const source = operation.source; - const sourceId = source?.data?.sourceDroppableId ?? null; - setSourceDroppableId(sourceId); - - if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - const defaultDestination: DropDestination = { - droppableId: - NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS, - index: orphanItemCount, - }; - setAddToNavigationFallbackDestination(defaultDestination); - setActiveDropTargetId( - getDndKitDropTargetId( - defaultDestination.droppableId, - defaultDestination.index, - ), - ); - } - }; - - const handleDragOver = useCallback( - (event: DragOverPayload) => { - const { operation } = event; - const source = operation.source; - const target = operation.target; - const isAddToNavDrag = - sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; - const resolved = resolveFavoritesDropTarget(target, getNavItemById); - - if ( - resolved !== null && - source !== null && - target !== null && - isSortable(source) && - isSortable(target) - ) { - setActiveDropTargetId(resolved.effectiveDropTargetId); - setForbiddenDropTargetId(null); - return; - } - - if (resolved !== null && source !== null && isSortable(source)) { - setActiveDropTargetId(resolved.effectiveDropTargetId); - setAddToNavigationFallbackDestination(resolved.destination); - setForbiddenDropTargetId(null); - return; - } - - if (!isAddToNavDrag) { - return; - } - - if (resolved !== null) { - setAddToNavigationFallbackDestination(resolved.destination); - setActiveDropTargetId(resolved.effectiveDropTargetId); - setForbiddenDropTargetId(null); - return; - } - - const fallback = addToNavigationFallbackDestination; - setActiveDropTargetId( - fallback - ? getDndKitDropTargetId(fallback.droppableId, fallback.index) - : null, - ); - setForbiddenDropTargetId(null); - }, - [ - sourceDroppableId, - addToNavigationFallbackDestination, - getNavItemById, - setActiveDropTargetId, - setForbiddenDropTargetId, - setAddToNavigationFallbackDestination, - ], - ); - - const handleDragEnd = (event: DragEndPayload) => { - const { operation } = event; - const source = operation.source; - const target = operation.target; - const draggableId = String(source?.id); - const data = source?.data; - const sourceId = data?.sourceDroppableId ?? null; - const fallback = addToNavigationFallbackDestination; - - setIsDragging(false); - setSourceDroppableId(null); - setActiveDropTargetId(null); - setForbiddenDropTargetId(null); - setAddToNavigationFallbackDestination(null); - - const resolved = resolveFavoritesDropTarget(target, getNavItemById); - let destination: DropDestination | null = resolved?.destination ?? null; - - if (destination == null && isDefined(fallback)) { - destination = fallback; - } - - const result = toDropResult(draggableId, data, destination); - const dropResult = { ...result, ...DROP_RESULT_OPTIONS }; - const provided: ResponderProvided = { announce: () => {} }; - - if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - handleAddToNavigationDrop(dropResult, provided); - return; - } - - handleNavigationMenuItemDragAndDrop(dropResult, provided); - }; - - const contextValues: FavoritesDndKitContextValues = { - dragSource: { sourceDroppableId }, - drag: { isDragging }, - dropTarget: { - activeDropTargetId, - setActiveDropTargetId, - forbiddenDropTargetId, - setForbiddenDropTargetId, - addToNavigationFallbackDestination, - }, - }; - - return { - contextValues, - handlers: { - onDragStart: handleDragStart, - onDragOver: handleDragOver, - onDragEnd: handleDragEnd, - }, - }; -}; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts b/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts deleted file mode 100644 index 0bf9a6c261..0000000000 --- a/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { type DragDropProvider } from '@dnd-kit/react'; -import { isSortable } from '@dnd-kit/react/sortable'; -import type { ResponderProvided } from '@hello-pangea/dnd'; -import { useStore } from 'jotai'; -import { type ComponentProps, useCallback, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; - -import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; -import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop'; -import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; -import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId'; -import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId'; - -import { DROP_RESULT_OPTIONS } from '@/navigation/constants/workspaceDndKitDropResultOptions'; -import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; -import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; -import { isFolderDrag } from '@/navigation/utils/workspaceDndKitIsFolderDrag'; -import { resolveDropTarget } from '@/navigation/utils/workspaceDndKitResolveDropTarget'; -import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult'; - -type DragStartPayload = Parameters< - NonNullable< - ComponentProps>['onDragStart'] - > ->[0]; -type DragOverPayload = Parameters< - NonNullable< - ComponentProps>['onDragOver'] - > ->[0]; -type DragEndPayload = Parameters< - NonNullable< - ComponentProps>['onDragEnd'] - > ->[0]; - -export type WorkspaceDndKitContextValues = { - dragSource: { sourceDroppableId: string | null }; - drag: { isDragging: boolean }; - dropTarget: { - activeDropTargetId: string | null; - setActiveDropTargetId: (id: string | null) => void; - forbiddenDropTargetId: string | null; - setForbiddenDropTargetId: (id: string | null) => void; - addToNavigationFallbackDestination: DropDestination | null; - }; -}; - -export const useWorkspaceDndKit = (): { - contextValues: WorkspaceDndKitContextValues; - handlers: { - onDragStart: (event: DragStartPayload) => void; - onDragOver: (event: DragOverPayload) => void; - onDragEnd: (event: DragEndPayload) => void; - }; -} => { - const store = useStore(); - const [isDragging, setIsDragging] = useState(false); - const [sourceDroppableId, setSourceDroppableId] = useState( - null, - ); - const [activeDropTargetId, setActiveDropTargetId] = useState( - null, - ); - const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< - string | null - >(null); - const [ - addToNavigationFallbackDestination, - setAddToNavigationFallbackDestination, - ] = useState(null); - - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop(); - const { handleWorkspaceNavigationMenuItemDragAndDrop } = - useHandleWorkspaceNavigationMenuItemDragAndDrop(); - - const orphanItemCount = workspaceNavigationMenuItems.filter( - (item: { folderId?: string | null }) => !isDefined(item.folderId), - ).length; - - const getNavItemById = useCallback( - (id: string | undefined) => - id - ? workspaceNavigationMenuItems.find((item) => item.id === id) - : undefined, - [workspaceNavigationMenuItems], - ); - - const applyWorkspaceReorderIfAllowed = ( - id: string, - source: DropDestination, - destination: DropDestination, - insertBeforeItemId?: string | null, - ) => { - const draggedItem = getNavItemById(id); - const destFolderId = validateAndExtractWorkspaceFolderId( - destination.droppableId, - ); - if ( - isDefined(destFolderId) && - isDefined(draggedItem) && - isNavigationMenuItemFolder(draggedItem) - ) { - return; - } - const result = toDropResult( - id, - { - sourceDroppableId: source.droppableId, - sourceIndex: source.index, - }, - destination, - ); - const provided: ResponderProvided = { announce: () => {} }; - handleWorkspaceNavigationMenuItemDragAndDrop( - { - ...result, - ...DROP_RESULT_OPTIONS, - ...(insertBeforeItemId != null && { insertBeforeItemId }), - }, - provided, - ); - }; - - const handleDragStart = (event: DragStartPayload) => { - const { operation } = event; - setIsDragging(true); - const source = operation.source; - const sourceId = source?.data?.sourceDroppableId ?? null; - setSourceDroppableId(sourceId); - - if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - const defaultDestination: DropDestination = { - droppableId: - NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, - index: orphanItemCount, - }; - setAddToNavigationFallbackDestination(defaultDestination); - setActiveDropTargetId( - getDndKitDropTargetId( - defaultDestination.droppableId, - defaultDestination.index, - ), - ); - } - }; - - const handleDragOver = useCallback( - (event: DragOverPayload) => { - const { operation } = event; - const source = operation.source; - const target = operation.target; - const isAddToNavDrag = - sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; - const sourceIsSortable = source !== null && isSortable(source); - const resolved = resolveDropTarget(target, getNavItemById); - - const getPayload = () => - store.get(addToNavPayloadRegistryState.atom).get(String(source?.id)) ?? - null; - const getSourceItem = () => - getNavItemById(source?.id != null ? String(source.id) : undefined); - - if ( - resolved !== null && - source !== null && - target !== null && - isSortable(source) && - isSortable(target) - ) { - setActiveDropTargetId(resolved.effectiveDropTargetId); - if (isAddToNavDrag) { - setForbiddenDropTargetId(null); - } else { - const destFolderId = - 'group' in target - ? validateAndExtractWorkspaceFolderId(String(target.group)) - : validateAndExtractWorkspaceFolderId( - resolved.destination.droppableId, - ); - const folderDrag = isFolderDrag(getPayload(), getSourceItem()); - const isFolderOverFolder = resolved.isTargetFolder && folderDrag; - const isFolderOverFolderInList = - !resolved.isTargetFolder && isDefined(destFolderId) && folderDrag; - setForbiddenDropTargetId( - isFolderOverFolder - ? resolved.effectiveDropTargetId - : isFolderOverFolderInList - ? resolved.dropTargetId - : null, - ); - } - return; - } - - if (resolved !== null && sourceIsSortable) { - setActiveDropTargetId(resolved.effectiveDropTargetId); - setAddToNavigationFallbackDestination(resolved.destination); - if (!isAddToNavDrag) { - const destFolderId = validateAndExtractWorkspaceFolderId( - resolved.destination.droppableId, - ); - const folderDrag = isFolderDrag(null, getSourceItem()); - setForbiddenDropTargetId( - isDefined(destFolderId) && folderDrag - ? resolved.effectiveDropTargetId - : null, - ); - } else { - setForbiddenDropTargetId(null); - } - return; - } - - if (!isAddToNavDrag) { - return; - } - - if (resolved !== null) { - setAddToNavigationFallbackDestination(resolved.destination); - setActiveDropTargetId(resolved.effectiveDropTargetId); - const folderId = validateAndExtractWorkspaceFolderId( - resolved.destination.droppableId, - ); - const folderDrag = - getPayload()?.type === 'FOLDER' && isDefined(folderId); - setForbiddenDropTargetId( - folderDrag ? resolved.effectiveDropTargetId : null, - ); - return; - } - - const fallback = addToNavigationFallbackDestination; - setActiveDropTargetId( - fallback - ? getDndKitDropTargetId(fallback.droppableId, fallback.index) - : null, - ); - setForbiddenDropTargetId(null); - }, - [ - sourceDroppableId, - addToNavigationFallbackDestination, - getNavItemById, - setActiveDropTargetId, - setForbiddenDropTargetId, - setAddToNavigationFallbackDestination, - store, - ], - ); - - const handleDragEnd = (event: DragEndPayload) => { - const { operation } = event; - const source = operation.source; - const target = operation.target; - const draggableId = String(source?.id); - const data = source?.data; - const sourceId = data?.sourceDroppableId ?? null; - const fallback = addToNavigationFallbackDestination; - - setIsDragging(false); - setSourceDroppableId(null); - setActiveDropTargetId(null); - setForbiddenDropTargetId(null); - setAddToNavigationFallbackDestination(null); - - const sourceIsSortable = source !== null && isSortable(source); - const targetIsSortable = target !== null && isSortable(target); - const sortableToSortable = - sourceIsSortable && - targetIsSortable && - isDefined(source) && - isDefined(target); - const resolved = resolveDropTarget(target, getNavItemById); - - if (sortableToSortable && resolved !== null) { - const sourceDraggable = 'initialGroup' in source ? source : null; - const initialGroup = sourceDraggable?.initialGroup ?? ''; - const initialIndex = sourceDraggable?.initialIndex ?? 0; - const initialGroupStr = String(initialGroup); - const destGroup = String(target.group ?? ''); - const bothWorkspace = - isWorkspaceDroppableId(initialGroupStr) && - isWorkspaceDroppableId(destGroup); - if (bothWorkspace) { - const insertBeforeItemId = resolved.isTargetFolder - ? null - : String(target?.id ?? ''); - applyWorkspaceReorderIfAllowed( - draggableId, - { droppableId: initialGroupStr, index: initialIndex }, - resolved.destination, - insertBeforeItemId || undefined, - ); - return; - } - } - - let destination: DropDestination | null = resolved?.destination ?? null; - if ( - destination == null && - isDefined(fallback) && - isWorkspaceDroppableId(fallback.droppableId) - ) { - destination = fallback; - } - - const result = toDropResult(draggableId, data, destination); - const provided: ResponderProvided = { announce: () => {} }; - const dropResult = { ...result, ...DROP_RESULT_OPTIONS }; - - if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - handleAddToNavigationDrop(dropResult, provided); - return; - } - - if ( - isDefined(sourceId) && - isWorkspaceDroppableId(sourceId) && - isDefined(destination) && - isWorkspaceDroppableId(destination.droppableId) - ) { - applyWorkspaceReorderIfAllowed( - draggableId, - { - droppableId: data?.sourceDroppableId ?? '', - index: data?.sourceIndex ?? 0, - }, - destination, - ); - } - }; - - const contextValues: WorkspaceDndKitContextValues = { - dragSource: { sourceDroppableId }, - drag: { isDragging }, - dropTarget: { - activeDropTargetId, - setActiveDropTargetId, - forbiddenDropTargetId, - setForbiddenDropTargetId, - addToNavigationFallbackDestination, - }, - }; - - return { - contextValues, - handlers: { - onDragStart: handleDragStart, - onDragOver: handleDragOver, - onDragEnd: handleDragEnd, - }, - }; -}; diff --git a/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts b/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts deleted file mode 100644 index 3e2e97654a..0000000000 --- a/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts +++ /dev/null @@ -1,16 +0,0 @@ -let preloadScheduled = false; - -const preload = () => { - void import('@/navigation/components/WorkspaceDndKitProvider'); - void import( - '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit' - ); -}; - -export const preloadWorkspaceDndKit = (): void => { - if (preloadScheduled) { - return; - } - preloadScheduled = true; - preload(); -}; diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts deleted file mode 100644 index ad1c318912..0000000000 --- a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; -import { isDefined } from 'twenty-shared/utils'; - -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; - -type AddToNavPayload = { type?: string } | null; - -export const isFolderDrag = ( - payload: AddToNavPayload, - sourceItem: NavigationMenuItem | undefined, -): boolean => - payload?.type === 'FOLDER' || - (isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem)); diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx index 455f9823f0..efedde575a 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx @@ -1,8 +1,8 @@ import { styled } from '@linaria/react'; -import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly'; +import { WorkspaceSectionListReadOnly } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly'; import { WorkspaceSectionAddMenuItemButton } from '@/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton'; -import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps'; +import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps'; import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledFallback = styled.div` @@ -12,7 +12,7 @@ const StyledFallback = styled.div` `; type WorkspaceSectionListEditModeFallbackProps = Pick< - WorkspaceSectionListDndKitProps, + NavigationMenuItemSectionListDndKitProps, 'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick' >; @@ -22,7 +22,7 @@ export const WorkspaceSectionListEditModeFallback = ({ onActiveObjectMetadataItemClick, }: WorkspaceSectionListEditModeFallbackProps) => ( - { const selectedNavigationMenuItemInEditMode = useAtomStateValue( selectedNavigationMenuItemInEditModeState, ); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); const { updateFolderInDraft } = useUpdateFolderInDraft(); const defaultLabel = t`New folder`; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx index e9f5ef9666..8a7642ca5e 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx @@ -4,7 +4,7 @@ import { IconLink, IconWorld } from 'twenty-ui/display'; import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay'; import { NavigationMenuItemType } from 'twenty-shared/types'; import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft'; -import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState'; import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout'; import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; @@ -24,7 +24,7 @@ export const SidePanelLinkInfo = () => { const selectedNavigationMenuItemInEditMode = useAtomStateValue( selectedNavigationMenuItemInEditModeState, ); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); const { updateLinkInDraft } = useUpdateLinkInDraft(); const defaultLabel = t`Link label`; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfo.tsx index 58a2757c6a..5cab43810b 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfo.tsx @@ -6,7 +6,7 @@ import { } from 'twenty-ui/display'; import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types'; -import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState'; import { SidePanelAskAIInfo } from '@/side-panel/components/SidePanelAskAIInfo'; import { SidePanelFolderInfo } from '@/side-panel/components/SidePanelFolderInfo'; @@ -37,7 +37,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => { const selectedNavigationMenuItemInEditMode = useAtomStateValue( selectedNavigationMenuItemInEditModeState, ); - const items = useWorkspaceSectionItems(); + const items = useNavigationMenuItemSectionItems(); if (!isDefined(pageChip)) { return null; diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/__tests__/calculateNewPosition.test.ts b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/__tests__/calculateNewPosition.test.ts index e581b72923..145f3cf567 100644 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/__tests__/calculateNewPosition.test.ts +++ b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/__tests__/calculateNewPosition.test.ts @@ -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', () => { diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/calculateNewPosition.ts b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/calculateNewPosition.ts index 72fa437574..f10fcddcbb 100644 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/calculateNewPosition.ts +++ b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/calculateNewPosition.ts @@ -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 ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/folderDroppableIds.ts b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/folderDroppableIds.ts deleted file mode 100644 index f0fa04702f..0000000000 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/folderDroppableIds.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const FOLDER_DROPPABLE_IDS = { - FOLDER_PREFIX: 'folder-', - FOLDER_HEADER_PREFIX: 'folder-header-', -} as const; diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/validateAndExtractFolderId.ts b/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/validateAndExtractFolderId.ts deleted file mode 100644 index 536c610401..0000000000 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/utils/validateAndExtractFolderId.ts +++ /dev/null @@ -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', - ); -}; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx index 7abb03d9e7..45affd9dc7 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx @@ -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';