Navbar drag drop using dnd kit (#18288)
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
type OnDragUpdateResponder,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination';
|
||||
import { getFavoritesDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type FavoritesDragDropProviderContentProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const FavoritesDragDropProviderContent = ({
|
||||
children,
|
||||
}: FavoritesDragDropProviderContentProps) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<{ droppableId: string; index: number } | null>(null);
|
||||
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
|
||||
const isFavoritesDroppableId = (droppableId: string) =>
|
||||
droppableId ===
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS ||
|
||||
droppableId.startsWith('folder-');
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const handleDragStart = (dragStart: DragStart) => {
|
||||
setIsDragging(true);
|
||||
setSourceDroppableId(dragStart.source.droppableId);
|
||||
if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragUpdate = (update: Parameters<OnDragUpdateResponder>[0]) => {
|
||||
const { source, destination } = update;
|
||||
|
||||
if (source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
if (
|
||||
destination !== null &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
setAddToNavigationFallbackDestination(destination);
|
||||
const dropTargetId = getDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
|
||||
const payload =
|
||||
store
|
||||
.get(addToNavPayloadRegistryState.atom)
|
||||
.get(update.draggableId) ?? null;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const isFolderOverFolder =
|
||||
payload?.type === 'folder' && folderId !== null;
|
||||
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback ? getDropTargetIdFromDestination(fallback) : null,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(source.droppableId)) {
|
||||
if (isDefined(destination)) {
|
||||
const dropTargetId =
|
||||
getFavoritesDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
} else {
|
||||
setActiveDropTargetId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
const isAddToNavigationSource =
|
||||
result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const effectiveResult: DropResult =
|
||||
isAddToNavigationSource &&
|
||||
!result.destination &&
|
||||
addToNavigationFallbackDestination
|
||||
? { ...result, destination: addToNavigationFallbackDestination }
|
||||
: result;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
if (isAddToNavigationSource) {
|
||||
handleAddToNavigationDrop(effectiveResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(result.source.droppableId)) {
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
|
||||
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</FavoritesDragContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
};
|
||||
+4
-4
@@ -1,9 +1,11 @@
|
||||
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
|
||||
import { NavigationDrawerWorkspaceSectionSkeletonLoader } from '@/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader';
|
||||
import { RemoteNavigationDrawerSection } from '@/object-metadata/components/RemoteNavigationDrawerSection';
|
||||
|
||||
import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = lazy(() =>
|
||||
@@ -32,10 +34,8 @@ export const MainNavigationDrawerScrollableItems = () => {
|
||||
return (
|
||||
<StyledScrollableItemsContainer>
|
||||
<NavigationDrawerOpenedSection />
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<NavigationDrawerWorkspaceSectionSkeletonLoader />}>
|
||||
<CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher />
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<WorkspaceNavigationMenuItemsDispatcher />
|
||||
</Suspense>
|
||||
<RemoteNavigationDrawerSection />
|
||||
|
||||
+22
-16
@@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconHelpCircle, IconSettings } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
@@ -54,24 +55,29 @@ export const NavigationDrawerOtherSection = () => {
|
||||
<NavigationDrawerSectionTitle
|
||||
label={t`Other`}
|
||||
onClick={toggleNavigationSection}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
Icon={IconSettings}
|
||||
onClick={handleSettingsClick}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Documentation`}
|
||||
to={getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
})}
|
||||
Icon={IconHelpCircle}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
Icon={IconSettings}
|
||||
onClick={handleSettingsClick}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Documentation`}
|
||||
to={getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
})}
|
||||
Icon={IconHelpCircle}
|
||||
/>
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
type OnDragUpdateResponder,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { lazy, Suspense, useState, type ReactNode } from 'react';
|
||||
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useStore } from 'jotai';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageDragDropProviderMountEffect } from '@/navigation/components/PageDragDropProviderMountEffect';
|
||||
|
||||
const LazyWorkspaceDndKitProvider = lazy(() =>
|
||||
import('@/navigation/components/WorkspaceDndKitProvider').then((m) => ({
|
||||
default: m.WorkspaceDndKitProvider,
|
||||
})),
|
||||
);
|
||||
|
||||
type PageDragDropProviderProps = {
|
||||
children: ReactNode;
|
||||
@@ -34,144 +15,22 @@ type PageDragDropProviderProps = {
|
||||
export const PageDragDropProvider = ({
|
||||
children,
|
||||
}: PageDragDropProviderProps) => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<{ droppableId: string; index: number } | null>(null);
|
||||
const [hasProviderMounted, setHasProviderMounted] = useState(false);
|
||||
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
|
||||
useHandleWorkspaceNavigationMenuItemDragAndDrop();
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const handleDragStart = (dragStart: DragStart) => {
|
||||
setIsDragging(true);
|
||||
setSourceDroppableId(dragStart.source.droppableId);
|
||||
if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragUpdate = useCallback(
|
||||
((update: Parameters<OnDragUpdateResponder>[0]) => {
|
||||
const { source, destination } = update;
|
||||
if (source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
destination !== null &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
setAddToNavigationFallbackDestination(destination);
|
||||
const dropTargetId = getDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
|
||||
const payload =
|
||||
store
|
||||
.get(addToNavPayloadRegistryState.atom)
|
||||
.get(update.draggableId) ?? null;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const isFolderOverFolder =
|
||||
payload?.type === 'folder' && folderId !== null;
|
||||
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback ? getDropTargetIdFromDestination(fallback) : null,
|
||||
);
|
||||
}
|
||||
}) as OnDragUpdateResponder,
|
||||
[addToNavigationFallbackDestination, store],
|
||||
);
|
||||
|
||||
const handleDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
const isAddToNavigationSource =
|
||||
result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const effectiveResult: DropResult =
|
||||
isAddToNavigationSource &&
|
||||
!result.destination &&
|
||||
addToNavigationFallbackDestination
|
||||
? { ...result, destination: addToNavigationFallbackDestination }
|
||||
: result;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
if (isAddToNavigationSource) {
|
||||
handleAddToNavigationDrop(effectiveResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
const isWorkspaceDrop =
|
||||
isWorkspaceDroppableId(result.source?.droppableId) &&
|
||||
isWorkspaceDroppableId(result.destination?.droppableId);
|
||||
if (isWorkspaceDrop) {
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(result, provided);
|
||||
} else {
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
}
|
||||
} else {
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
}
|
||||
};
|
||||
if (!hasProviderMounted) {
|
||||
return (
|
||||
<>
|
||||
<PageDragDropProviderMountEffect
|
||||
onEnterEditMode={() => setHasProviderMounted(true)}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
|
||||
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</FavoritesDragContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
<Suspense fallback={<>{children}</>}>
|
||||
<LazyWorkspaceDndKitProvider>{children}</LazyWorkspaceDndKitProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type PageDragDropProviderMountEffectProps = {
|
||||
onEnterEditMode: () => void;
|
||||
};
|
||||
|
||||
export const PageDragDropProviderMountEffect = ({
|
||||
onEnterEditMode,
|
||||
}: PageDragDropProviderMountEffectProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNavigationMenuInEditMode) {
|
||||
onEnterEditMode();
|
||||
}
|
||||
}, [isNavigationMenuInEditMode, onEnterEditMode]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
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/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/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 (
|
||||
<NavigationDragSourceContext.Provider value={contextValues.dragSource}>
|
||||
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
||||
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
||||
<DragDropProvider<DraggableData>
|
||||
sensors={WORKSPACE_DND_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragOver={handlers.onDragOver}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropProvider>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const DROP_RESULT_OPTIONS = {
|
||||
reason: 'DROP' as const,
|
||||
combine: null,
|
||||
mode: 'FLUID' as const,
|
||||
type: 'DEFAULT' as const,
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
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 { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/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<typeof DragDropProvider<DraggableData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragOverPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['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<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<DropDestination | null>(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,
|
||||
) => {
|
||||
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 },
|
||||
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) {
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{ droppableId: initialGroupStr, index: initialIndex },
|
||||
resolved.destination,
|
||||
);
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
let preloadScheduled = false;
|
||||
|
||||
const preload = () => {
|
||||
void import('@/navigation/components/WorkspaceDndKitProvider');
|
||||
void import(
|
||||
'@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
|
||||
);
|
||||
};
|
||||
|
||||
export const preloadWorkspaceDndKit = (): void => {
|
||||
if (preloadScheduled) {
|
||||
return;
|
||||
}
|
||||
preloadScheduled = true;
|
||||
preload();
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type DraggableData = {
|
||||
sourceDroppableId?: string;
|
||||
sourceIndex?: number;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type DropDestination = { droppableId: string; index: number };
|
||||
@@ -0,0 +1,4 @@
|
||||
export type DroppableData = {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
|
||||
export type SortableTargetDestination = {
|
||||
destination: DropDestination;
|
||||
effectiveDropTargetId: string;
|
||||
isTargetFolder: boolean;
|
||||
dropTargetId: string;
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
) => NavigationMenuItem | undefined;
|
||||
|
||||
export const getDestinationFromSortableTarget = (
|
||||
target: { id: unknown; group?: unknown; index?: unknown },
|
||||
getNavItemById: GetNavItemById,
|
||||
): SortableTargetDestination | null => {
|
||||
const group = target.group;
|
||||
const rawIndex = target.index;
|
||||
if (!isDefined(group) || !isDefined(rawIndex)) {
|
||||
return null;
|
||||
}
|
||||
const index = Number(rawIndex);
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
return null;
|
||||
}
|
||||
const destDroppableId = String(group);
|
||||
const targetItem = getNavItemById(
|
||||
target.id != null ? String(target.id) : undefined,
|
||||
);
|
||||
const isTargetFolder =
|
||||
isDefined(targetItem) && isNavigationMenuItemFolder(targetItem);
|
||||
const dropTargetId = getDndKitDropTargetId(destDroppableId, index);
|
||||
const effectiveDropTargetId = isTargetFolder
|
||||
? getDndKitDropTargetId(
|
||||
`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`,
|
||||
0,
|
||||
)
|
||||
: dropTargetId;
|
||||
const destination: DropDestination = {
|
||||
droppableId: isTargetFolder
|
||||
? `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`
|
||||
: destDroppableId,
|
||||
index: isTargetFolder ? 0 : index,
|
||||
};
|
||||
return {
|
||||
destination,
|
||||
effectiveDropTargetId,
|
||||
isTargetFolder,
|
||||
dropTargetId,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
type AddToNavPayload = { type?: string } | null;
|
||||
|
||||
export const isFolderDrag = (
|
||||
payload: AddToNavPayload,
|
||||
sourceItem: NavigationMenuItem | undefined,
|
||||
): boolean =>
|
||||
payload?.type === 'folder' ||
|
||||
(isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem));
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
|
||||
import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
) => NavigationMenuItem | undefined;
|
||||
|
||||
const isDroppableData = (data: unknown): data is DroppableData =>
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
typeof (data as DroppableData).droppableId === 'string' &&
|
||||
typeof (data as DroppableData).index === 'number';
|
||||
|
||||
export const resolveDropTarget = (
|
||||
target: {
|
||||
id?: unknown;
|
||||
group?: unknown;
|
||||
index?: unknown;
|
||||
data?: unknown;
|
||||
} | null,
|
||||
getNavItemById: GetNavItemById,
|
||||
): SortableTargetDestination | null => {
|
||||
if (target === null || target === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (isDefined(target.group) && isDefined(target.index)) {
|
||||
return getDestinationFromSortableTarget(
|
||||
{ id: target.id, group: target.group, index: target.index },
|
||||
getNavItemById,
|
||||
);
|
||||
}
|
||||
if (isDroppableData(target.data)) {
|
||||
const { droppableId, index } = target.data;
|
||||
if (isWorkspaceDroppableId(droppableId)) {
|
||||
return {
|
||||
destination: { droppableId, index },
|
||||
effectiveDropTargetId: String(target.id),
|
||||
isTargetFolder: false,
|
||||
dropTargetId: String(target.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
|
||||
export const toDropResult = (
|
||||
draggableId: string,
|
||||
data: DraggableData | undefined,
|
||||
destination: DropDestination | null,
|
||||
): {
|
||||
source: DropDestination;
|
||||
destination: DropDestination | null;
|
||||
draggableId: string;
|
||||
} => {
|
||||
const sourceDroppableId = data?.sourceDroppableId ?? '';
|
||||
const sourceIndex = data?.sourceIndex ?? 0;
|
||||
return {
|
||||
source: { droppableId: sourceDroppableId, index: sourceIndex },
|
||||
destination,
|
||||
draggableId,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user