From c1ec5567ce7235a34cdbab8c704fdabc8bef4ed3 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:32:01 +0530 Subject: [PATCH] fix: insert-before folder for workspace DnD and add-to-nav drags (#18789) Closes [#2294](https://github.com/twentyhq/core-team-issues/issues/2294) https://github.com/user-attachments/assets/f7e0da72-dd08-46c4-92d7-be0453aaadd9 --------- Co-authored-by: Charles Bochet Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../navigationMenuItemDndKitDroppableData.ts | 1 + ...MenuItemDndKitSortableTargetDestination.ts | 1 + .../components/NavigationMenuItemDisplay.tsx | 2 + .../NavigationMenuItemDroppableSlot.tsx | 7 +- ...ationMenuItemInsertBeforeDroppableZone.tsx | 48 +++++++++++++ .../dnd/hooks/useHandleAddToNavigationDrop.ts | 21 +++--- .../useHandleNavigationMenuItemDragAndDrop.ts | 40 +++++------ .../dnd/hooks/useNavigationMenuItemDndKit.ts | 31 +++++---- ...mDndKitGetDestinationFromSortableTarget.ts | 20 ++++-- ...vigationMenuItemDndKitResolveDropTarget.ts | 3 +- .../components/NavigationMenuItemFolder.tsx | 3 + .../NavigationMenuItemFolderDnd.tsx | 21 +++++- .../useNavigationMenuItemSectionItems.ts | 57 +++------------ .../NavigationMenuItemSectionContentProps.ts | 1 + .../components/WorkspaceSectionListDndKit.tsx | 1 + ...rkspaceSidebarOrphanItemsInDisplayOrder.ts | 69 +++++++++++++++++++ 16 files changed, 217 insertions(+), 109 deletions(-) create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemInsertBeforeDroppableZone.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts index 3facdbf799..3f33e3c68c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData.ts @@ -1,4 +1,5 @@ export type DroppableData = { droppableId: string; index: number; + insertBeforeItemId?: string; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts index a447d2f6a7..171f41dcb2 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination.ts @@ -5,4 +5,5 @@ export type SortableTargetDestination = { effectiveDropTargetId: string; isTargetFolder: boolean; dropTargetId: string; + insertBeforeItemId?: string; }; 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 ef71897ef9..0f2d7f91a4 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 @@ -18,6 +18,7 @@ export const NavigationMenuItemDisplay = ({ onNavigationMenuItemClick, onActiveObjectMetadataItemClick, readOnly, + orphanIndex, }: NavigationMenuItemDisplayProps) => { switch (item.type) { case NavigationMenuItemType.FOLDER: @@ -32,6 +33,7 @@ export const NavigationMenuItemDisplay = ({ selectedNavigationMenuItemId={selectedNavigationMenuItemId} onNavigationMenuItemClick={onNavigationMenuItemClick} readOnly={readOnly} + orphanIndex={orphanIndex} /> ); case NavigationMenuItemType.LINK: diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot.tsx index 1681b59d60..f986b5d843 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot.tsx @@ -1,10 +1,11 @@ +import { pointerIntersection } from '@dnd-kit/collision'; import { useDroppable } from '@dnd-kit/react'; import { styled } from '@linaria/react'; import { type ReactNode } from 'react'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; import type { DroppableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; const StyledSlotWrapper = styled.div<{ $empty: boolean }>` min-height: 0; @@ -13,8 +14,7 @@ const StyledSlotWrapper = styled.div<{ $empty: boolean }>` `; const SLOT_COLLISION_PRIORITY = 1; - -export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 2; +export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 4; type NavigationMenuItemDroppableSlotProps = { droppableId: string; @@ -37,6 +37,7 @@ export const NavigationMenuItemDroppableSlot = ({ id, disabled, collisionPriority, + collisionDetector: pointerIntersection, data, }); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemInsertBeforeDroppableZone.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemInsertBeforeDroppableZone.tsx new file mode 100644 index 0000000000..05b5eceb92 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationMenuItemInsertBeforeDroppableZone.tsx @@ -0,0 +1,48 @@ +import { pointerIntersection } from '@dnd-kit/collision'; +import { useDroppable } from '@dnd-kit/react'; +import { styled } from '@linaria/react'; + +import type { DroppableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId'; + +const INSERT_BEFORE_ZONE_HEIGHT_PX = 5; + +const StyledInsertBeforeZone = styled.div` + height: ${INSERT_BEFORE_ZONE_HEIGHT_PX}px; + left: 0; + pointer-events: auto; + position: absolute; + right: 0; + top: -${INSERT_BEFORE_ZONE_HEIGHT_PX}px; + z-index: 1; +`; + +type NavigationMenuItemInsertBeforeDroppableZoneProps = { + orphanDroppableId: string; + orphanIndex: number; + itemId: string; + disabled?: boolean; +}; + +export const NavigationMenuItemInsertBeforeDroppableZone = ({ + orphanDroppableId, + orphanIndex, + itemId, + disabled = false, +}: NavigationMenuItemInsertBeforeDroppableZoneProps) => { + const id = getDndKitDropTargetId(orphanDroppableId, orphanIndex); + const data: DroppableData = { + droppableId: orphanDroppableId, + index: orphanIndex, + insertBeforeItemId: itemId, + }; + const { ref } = useDroppable({ + id, + disabled, + collisionPriority: 5, + collisionDetector: pointerIntersection, + data, + }); + + return ; +}; 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 6b50cdb59a..544873a4d6 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 @@ -6,26 +6,27 @@ import { IconFolder, IconLink, useIcons } from 'twenty-ui/display'; import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; -import { NavigationMenuItemType } from 'twenty-shared/types'; +import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; +import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; +import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; +import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft'; +import { getStandardObjectIconColor } from '@/navigation-menu-item/common/utils/getStandardObjectIconColor'; +import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId'; import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft'; +import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft'; import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft'; import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft'; import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; -import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -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 { 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'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; + import { useStore } from 'jotai'; +import { NavigationMenuItemType } from 'twenty-shared/types'; export const useHandleAddToNavigationDrop = () => { const store = useStore(); 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 a29848af9a..9ca21fc1f7 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 @@ -175,37 +175,31 @@ export const useHandleNavigationMenuItemDragAndDrop = ( return; } - if (sourceFolderId === destinationFolderId) { - const sourceList = getSortedItems().filter((item) => - matchesFolderId(item, sourceFolderId), - ); - - if (!sourceList.some((item) => item.id === draggableId)) return; - - const insertBeforeIndex = - result.insertBeforeItemId != null - ? sourceList.findIndex( - (item) => item.id === result.insertBeforeItemId, - ) - : -1; - - await computeAndApplyReorder( - draggableId, - sourceList, - insertBeforeIndex >= 0 ? insertBeforeIndex : destination.index, - ); - return; - } + const isSameFolder = sourceFolderId === destinationFolderId; const destinationList = getSortedItems().filter((item) => matchesFolderId(item, destinationFolderId), ); + if ( + isSameFolder && + !destinationList.some((item) => item.id === draggableId) + ) { + return; + } + + const insertBeforeIndex = + result.insertBeforeItemId != null + ? destinationList.findIndex( + (item) => item.id === result.insertBeforeItemId, + ) + : -1; + await computeAndApplyReorder( draggableId, destinationList, - destination.index, - destinationFolderId ?? null, + insertBeforeIndex >= 0 ? insertBeforeIndex : destination.index, + isSameFolder ? undefined : (destinationFolderId ?? null), ); }; 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 index b9f5440298..bbc43e7823 100644 --- 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 @@ -6,10 +6,12 @@ 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 { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; 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 { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination'; 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'; @@ -24,9 +26,6 @@ import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks 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'] @@ -234,16 +233,16 @@ export const useNavigationMenuItemDndKit = ( isSortable(source) && isSortable(target) ) { - setActiveDropTargetId(resolved.effectiveDropTargetId); - setForbiddenDropTargetId( - isAddToNavDrag - ? computeForbiddenTargetId(source, resolved, true) - : computeForbiddenTargetId(source, resolved, false), - ); + const forbiddenId = isAddToNavDrag + ? computeForbiddenTargetId(source, resolved, true) + : computeForbiddenTargetId(source, resolved, false); + + setActiveDropTargetId(resolved.dropTargetId); + setForbiddenDropTargetId(forbiddenId); return; } - // Branch 2: sortable-to-droppable-slot + // Branch 2: sortable-to-droppable-slot (includes insert-before zones) if (resolved !== null && sourceIsSortable) { setActiveDropTargetId(resolved.effectiveDropTargetId); setAddToNavigationFallbackDestination(resolved.destination); @@ -257,7 +256,7 @@ export const useNavigationMenuItemDndKit = ( return; } - // Branch 3: add-to-nav drag + // Branch 3: add-to-nav drag over droppable if (resolved !== null) { setAddToNavigationFallbackDestination(resolved.destination); setActiveDropTargetId(resolved.effectiveDropTargetId); @@ -326,20 +325,21 @@ export const useNavigationMenuItemDndKit = ( droppableId: destGroup, }); if (bothWorkspace) { - const insertBeforeItemId = resolved.isTargetFolder - ? null - : String(target?.id ?? ''); + const insertBeforeItemId = + target?.id != null ? String(target.id) : undefined; applyWorkspaceReorder( draggableId, { droppableId: initialGroup, index: initialIndex }, resolved.destination, - insertBeforeItemId || undefined, + insertBeforeItemId, ); return; } } let destination: DropDestination | null = resolved?.destination ?? null; + const insertBeforeItemId = resolved?.insertBeforeItemId; + if ( destination == null && isDefined(fallback) && @@ -380,6 +380,7 @@ export const useNavigationMenuItemDndKit = ( index: data?.sourceIndex ?? 0, }, destination, + insertBeforeItemId, ); } return; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts index 511d65bd07..cf6162cfa6 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget.ts @@ -34,18 +34,24 @@ export const getDestinationFromSortableTarget = ( isDefined(targetItem) && isNavigationMenuItemFolder(targetItem); const dropTargetId = getDndKitDropTargetId(destDroppableId, index); - const { folderHeaderPrefix } = + const { folderHeaderPrefix, orphanDroppableId } = NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection]; + const isTopLevelOrphanGroup = destDroppableId === orphanDroppableId; + const effectiveDropTargetId = isTargetFolder ? getDndKitDropTargetId(`${folderHeaderPrefix}${target.id}`, 0) : dropTargetId; - const destination: DropDestination = { - droppableId: isTargetFolder - ? `${folderHeaderPrefix}${target.id}` - : destDroppableId, - index: isTargetFolder ? 0 : index, - }; + + const destination: DropDestination = + isTargetFolder && isTopLevelOrphanGroup + ? { droppableId: destDroppableId, index } + : isTargetFolder + ? { + droppableId: `${folderHeaderPrefix}${String(target.id)}`, + index: 0, + } + : { droppableId: destDroppableId, index }; return { destination, effectiveDropTargetId, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts index 1319f8c6d1..4daf0007a0 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget.ts @@ -38,7 +38,7 @@ export const resolveDropTarget = ( ); } if (isDroppableData(target.data)) { - const { droppableId, index } = target.data; + const { droppableId, index, insertBeforeItemId } = target.data; if ( canNavigationMenuItemBeDroppedIn({ navigationMenuItemSection, @@ -50,6 +50,7 @@ export const resolveDropTarget = ( effectiveDropTargetId: String(target.id), isTargetFolder: false, dropTargetId: String(target.id), + insertBeforeItemId, }; } } 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 index a0e755ba7c..2188c3cbde 100644 --- 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 @@ -30,6 +30,7 @@ type NavigationMenuItemFolderProps = Pick< | 'selectedNavigationMenuItemId' | 'onNavigationMenuItemClick' | 'readOnly' + | 'orphanIndex' >; export const NavigationMenuItemFolder = ({ @@ -42,6 +43,7 @@ export const NavigationMenuItemFolder = ({ selectedNavigationMenuItemId, onNavigationMenuItemClick, readOnly = false, + orphanIndex, }: NavigationMenuItemFolderProps) => { const folderId = item.id; const folderName = item.name ?? 'Folder'; @@ -88,6 +90,7 @@ export const NavigationMenuItemFolder = ({ isDragging={isDragging} selectedNavigationMenuItemId={selectedNavigationMenuItemId} onNavigationMenuItemClick={onNavigationMenuItemClick} + orphanIndex={orphanIndex} /> ); 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 index 2081c4d1fe..aead0072cf 100644 --- 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 @@ -34,6 +34,7 @@ import { FOLDER_HEADER_SLOT_COLLISION_PRIORITY, NavigationMenuItemDroppableSlot, } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot'; +import { NavigationMenuItemInsertBeforeDroppableZone } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemInsertBeforeDroppableZone'; 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'; @@ -88,6 +89,7 @@ type NavigationMenuItemFolderDndProps = { isDragging: boolean; selectedNavigationMenuItemId?: string | null; onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; + orphanIndex?: number; }; export const NavigationMenuItemFolderDnd = ({ @@ -102,6 +104,7 @@ export const NavigationMenuItemFolderDnd = ({ isDragging: isDraggingProp, selectedNavigationMenuItemId, onNavigationMenuItemClick, + orphanIndex, }: NavigationMenuItemFolderDndProps) => { const { t } = useLingui(); const { theme } = useContext(ThemeContext); @@ -243,8 +246,22 @@ export const NavigationMenuItemFolderDnd = ({ /> ); + const showInsertBeforeZone = + isDragging && orphanIndex !== undefined && !isEditInPlace; + const wrappedHeader = ( -
+
+ {showInsertBeforeZone && ( + + )} { const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - const flatWorkspaceItems = workspaceNavigationMenuItems - .filter((item) => !isDefined(item.folderId)) - .sort((a, b) => a.position - b.position); - - const processedItemsById = new Map( - workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]), - ); - const folderChildrenById = new Map( workspaceNavigationMenuItemsByFolder.map((folder) => [ folder.id, @@ -45,40 +33,13 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { ]), ); - const flatItems: NavigationMenuItem[] = flatWorkspaceItems.reduce< - NavigationMenuItem[] - >((acc, item) => { - if (isNavigationMenuItemFolder(item)) { - acc.push({ - ...item, - icon: item.icon ?? FOLDER_ICON_DEFAULT, - }); - } else { - const validItem = processedItemsById.get(item.id); - if (!isDefined(validItem)) { - return acc; - } - if (validItem.type === NavigationMenuItemType.LINK) { - acc.push(validItem); - } else { - const objectMetadataItem = getObjectMetadataForNavigationMenuItem( - validItem, - objectMetadataItems, - views, - ); - if ( - isDefined(objectMetadataItem) && - getObjectPermissionsForObject( - objectPermissionsByObjectMetadataId, - objectMetadataItem.id, - ).canReadObjectRecords - ) { - acc.push(validItem); - } - } - } - return acc; - }, []); + const flatItems = getWorkspaceSidebarOrphanItemsInDisplayOrder({ + workspaceNavigationMenuItems, + workspaceNavigationMenuItemsSorted, + objectMetadataItems, + views, + objectPermissionsByObjectMetadataId, + }); return flatItems.flatMap((item) => item.type === NavigationMenuItemType.FOLDER diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts index f7393a1cd8..5b43f34f0e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps.ts @@ -21,4 +21,5 @@ export type NavigationMenuItemSectionContentProps = { navigationMenuItemId: string, ) => void; readOnly?: boolean; + orphanIndex?: number; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx index 619a654b45..2e4a4b0d0c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit.tsx @@ -75,6 +75,7 @@ export const WorkspaceSectionListDndKit = ({ selectedNavigationMenuItemId={selectedNavigationMenuItemId} onNavigationMenuItemClick={onNavigationMenuItemClick} onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick} + orphanIndex={index} /> diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts new file mode 100644 index 0000000000..0fc44fd7ed --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts @@ -0,0 +1,69 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { type ViewWithRelations } from '@/views/types/ViewWithRelations'; + +type GetWorkspaceSidebarOrphanItemsInDisplayOrderArgs = { + workspaceNavigationMenuItems: NavigationMenuItem[]; + workspaceNavigationMenuItemsSorted: NavigationMenuItem[]; + objectMetadataItems: EnrichedObjectMetadataItem[]; + views: ViewWithRelations[]; + objectPermissionsByObjectMetadataId: Parameters< + typeof getObjectPermissionsForObject + >[0]; +}; + +export const getWorkspaceSidebarOrphanItemsInDisplayOrder = ({ + workspaceNavigationMenuItems, + workspaceNavigationMenuItemsSorted, + objectMetadataItems, + views, + objectPermissionsByObjectMetadataId, +}: GetWorkspaceSidebarOrphanItemsInDisplayOrderArgs): NavigationMenuItem[] => { + const flatWorkspaceItems = workspaceNavigationMenuItems + .filter((item) => !isDefined(item.folderId)) + .sort((a, b) => a.position - b.position); + + const processedItemsById = new Map( + workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]), + ); + + return flatWorkspaceItems.reduce((acc, item) => { + if (isNavigationMenuItemFolder(item)) { + acc.push({ + ...item, + icon: item.icon ?? FOLDER_ICON_DEFAULT, + }); + } else { + const validItem = processedItemsById.get(item.id); + if (!isDefined(validItem)) { + return acc; + } + if (validItem.type === NavigationMenuItemType.LINK) { + acc.push(validItem); + } else { + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + validItem, + objectMetadataItems, + views, + ); + if ( + isDefined(objectMetadataItem) && + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ).canReadObjectRecords + ) { + acc.push(validItem); + } + } + } + return acc; + }, []); +};