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 <charles@twenty.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2026-03-24 19:32:01 +05:30
committed by GitHub
parent 7a1780e415
commit c1ec5567ce
16 changed files with 217 additions and 109 deletions
@@ -1,4 +1,5 @@
export type DroppableData = {
droppableId: string;
index: number;
insertBeforeItemId?: string;
};
@@ -5,4 +5,5 @@ export type SortableTargetDestination = {
effectiveDropTargetId: string;
isTargetFolder: boolean;
dropTargetId: string;
insertBeforeItemId?: string;
};
@@ -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:
@@ -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,
});
@@ -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 <StyledInsertBeforeZone ref={ref} />;
};
@@ -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();
@@ -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),
);
};
@@ -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<typeof DragDropProvider<DraggableData>>['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;
@@ -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,
@@ -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,
};
}
}
@@ -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}
/>
</Suspense>
);
@@ -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 = (
<div ref={setSortableDropTargetRef ?? undefined}>
<div
ref={setSortableDropTargetRef ?? undefined}
style={{ position: 'relative' }}
>
{showInsertBeforeZone && (
<NavigationMenuItemInsertBeforeDroppableZone
orphanDroppableId={config.orphanDroppableId}
orphanIndex={orphanIndex}
itemId={folderId}
disabled={dropDisabled}
/>
)}
<NavigationMenuItemDroppableSlot
droppableId={folderHeaderDroppableId}
index={0}
@@ -369,7 +386,7 @@ export const NavigationMenuItemFolderDnd = ({
folderId={folderId}
index={navigationMenuItems.length}
sectionId={sectionId}
compact={!(isWorkspace && isLayoutCustomizationModeEnabled)}
compact={isCompact}
dropTargetIdOverride={getDndKitDropTargetId(
folderContentDroppableId,
navigationMenuItems.length,
@@ -1,16 +1,12 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { getWorkspaceSidebarOrphanItemsInDisplayOrder } from '@/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { isDefined } from 'twenty-shared/utils';
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
@@ -30,14 +26,6 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => {
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
@@ -21,4 +21,5 @@ export type NavigationMenuItemSectionContentProps = {
navigationMenuItemId: string,
) => void;
readOnly?: boolean;
orphanIndex?: number;
};
@@ -75,6 +75,7 @@ export const WorkspaceSectionListDndKit = ({
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
orphanIndex={index}
/>
</NavigationMenuItemSortableItem>
</StyledListItemRow>
@@ -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<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;
}, []);
};