Skip to content
Merged
85 changes: 47 additions & 38 deletions web/core/components/workspace/sidebar/favorites/favorite-folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import { useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Correct the import order according to coding standards

The static analysis tool indicates that pointerOutsideOfPreview import should occur before setCustomNativeDragPreview. Please reorder the imports to comply with project conventions.

Apply this diff to fix the import order:

- import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
- import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
+ import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
+ import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
🧰 Tools
🪛 GitHub Check: lint-web

[failure] 7-7:
@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview import should occur before import of @atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview


import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import uniqBy from "lodash/uniqBy";
import { useParams } from "next/navigation";
import { PenSquare, Star, MoreHorizontal, ChevronRight, GripVertical } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { createRoot } from "react-dom/client";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Adjust the import order to follow project conventions

According to the static analysis tool, the import of react-dom/client should occur before the import of lucide-react. Please adjust the import statements accordingly.

Apply this diff to fix the import order:

- import { PenSquare, Star, MoreHorizontal, ChevronRight, GripVertical } from "lucide-react";
  import { Disclosure, Transition } from "@headlessui/react";
+ import { createRoot } from "react-dom/client";

+ import { PenSquare, Star, MoreHorizontal, ChevronRight, GripVertical } from "lucide-react";

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: lint-web

[failure] 14-14:
react-dom/client import should occur before import of lucide-react


// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
// ui
Expand All @@ -24,21 +28,23 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { FavoriteRoot } from "./favorite-items";
import { getDestinationStateSequence } from "./favorites.helpers";
import { NewFavoriteFolder } from "./new-fav-folder";
import { orderBy } from "lodash";

type Props = {
isLastChild: boolean;
favorite: IFavorite;
handleRemoveFromFavorites: (favorite: IFavorite) => void;
handleRemoveFromFavoritesFolder: (favoriteId: string) => void;
handleReorder: (favoriteId: string, sequence: number) => void;
};

export const FavoriteFolder: React.FC<Props> = (props) => {
const { favorite, handleRemoveFromFavorites, handleRemoveFromFavoritesFolder } = props;
const { favorite, handleRemoveFromFavorites, handleRemoveFromFavoritesFolder, handleReorder } = props;
// store hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();

const { isMobile } = usePlatformOS();
const { moveFavorite, getGroupedFavorites, groupedFavorites, moveFavoriteFolder } = useFavorite();
const { getGroupedFavorites, groupedFavorites, moveFavoriteToFolder } = useFavorite();
const { workspaceSlug } = useParams();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
Expand All @@ -53,8 +59,8 @@ export const FavoriteFolder: React.FC<Props> = (props) => {

!favorite.children && getGroupedFavorites(workspaceSlug.toString(), favorite.id);

const handleOnDrop = (source: string, destination: string) => {
moveFavorite(workspaceSlug.toString(), source, {
const handleMoveToFolder = (source: string, destination: string) => {
moveFavoriteToFolder(workspaceSlug.toString(), source, {
parent: destination,
})
.then(() => {
Expand All @@ -73,24 +79,6 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
});
};

const handleOnDropFolder = (payload: Partial<IFavorite>) => {
moveFavoriteFolder(workspaceSlug.toString(), favorite.id, payload)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Folder moved successfully.",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to move folder.",
});
});
};

useEffect(() => {
const element = elementRef.current;

Expand All @@ -101,22 +89,36 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
draggable({
element,
getInitialData: () => initialData,
onDragStart: () => setIsDragging(true),
// onDragStart: () => setIsDragging(true),
onGenerateDragPreview: ({ nativeSetDragImage }) =>{
setCustomNativeDragPreview({
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(
<div className="rounded flex gap-1 bg-custom-background-100 text-sm p-1 pr-2">
<div className="size-5 grid place-items-center flex-shrink-0">
<FavoriteFolderIcon />
</div>
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{favorite.name}</p>
</div>
);
return () => root.unmount();
},
nativeSetDragImage,
});
},
onDrop: (data) => {
setIsDraggedOver(false);
if (!data.location.current.dropTargets[0]) return;
const destinationData = data.location.current.dropTargets[0].data;

if (favorite.id && destinationData) {
const edge = extractClosestEdge(destinationData) || undefined;
const payload = {
id: favorite.id,
sequence: Math.round(
getDestinationStateSequence(groupedFavorites, destinationData.id as string, edge) || 0
),
};

handleOnDropFolder(payload);
const sequence = Math.round(
getDestinationStateSequence(groupedFavorites, destinationData.id as string, edge) || 0
);
handleReorder(favorite.id,sequence);
}
}, // canDrag: () => isDraggable,
}),
Expand All @@ -128,10 +130,14 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
element,
allowedEdges: ["top", "bottom"],
}),
onDragEnter: (args) => {
setIsDragging(true);
setIsDraggedOver(true);
args.source.data.is_folder && setClosestEdge(extractClosestEdge(args.self.data));
onDragEnter: ({source,self}) => {
const sourceId = source?.data?.id as string;
const destinationId = self?.data?.id as string | undefined;
if (groupedFavorites[sourceId].parent !== destinationId) {
setIsDraggedOver(true);
setIsDragging(true);
};
source.data.is_folder && setClosestEdge(extractClosestEdge(self.data));
},
onDragLeave: () => {
setIsDragging(false);
Expand All @@ -146,16 +152,18 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
setIsDraggedOver(false);
const sourceId = source?.data?.id as string | undefined;
const destinationId = self?.data?.id as string | undefined;

if (source.data.is_folder) return;
if (sourceId === destinationId) return;
if (!sourceId || !destinationId) return;
if (groupedFavorites[sourceId].parent === destinationId) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null check to prevent runtime errors

Similarly, ensure that groupedFavorites[sourceId] is defined before accessing .parent to prevent potential errors.

Apply this diff to add the necessary check:

  if (!sourceId || !destinationId) return;
- if (groupedFavorites[sourceId].parent === destinationId) return;
+ if (groupedFavorites[sourceId] && groupedFavorites[sourceId].parent === destinationId) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!sourceId || !destinationId) return;
if (groupedFavorites[sourceId].parent === destinationId) return;
if (!sourceId || !destinationId) return;
if (groupedFavorites[sourceId] && groupedFavorites[sourceId].parent === destinationId) return;

handleOnDrop(sourceId, destinationId);

handleMoveToFolder(sourceId, destinationId);
},
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef.current, isDragging, favorite.id, handleOnDrop]);
}, [elementRef.current, isDragging, favorite.id, handleMoveToFolder]);

useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));

Expand Down Expand Up @@ -316,14 +324,15 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
"px-2": !isSidebarCollapsed,
})}
>
{uniqBy(favorite.children, "id").map((child) => (
{orderBy(uniqBy(favorite.children, "id"),'sequence','desc').map((child) => (
<FavoriteRoot
key={child.id}
workspaceSlug={workspaceSlug.toString()}
favorite={child}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
favoriteMap={groupedFavorites}
handleReorder={handleReorder}
/>
))}
</Disclosure.Panel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import React, { FC, useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
import { observer } from "mobx-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
Expand All @@ -19,66 +22,129 @@ import {
import { useAppTheme } from "@/hooks/store";
import { useFavoriteItemDetails } from "@/hooks/use-favorite-item-details";

import { createRoot } from "react-dom/client";
import { DropIndicator } from "@plane/ui";
//constants
import { getDestinationStateSequence } from "../favorites.helpers";

type Props = {
workspaceSlug: string;
favorite: IFavorite;
favoriteMap: Record<string, IFavorite>;
handleRemoveFromFavorites: (favorite: IFavorite) => void;
handleRemoveFromFavoritesFolder: (favoriteId: string) => void;
handleReorder: (favoriteId: string, sequence: number) => void;
};

export const FavoriteRoot: FC<Props> = observer((props) => {
// props
const { workspaceSlug, favorite, favoriteMap, handleRemoveFromFavorites, handleRemoveFromFavoritesFolder } = props;
const {
workspaceSlug,
favorite,
favoriteMap,
handleRemoveFromFavorites,
handleRemoveFromFavoritesFolder,
handleReorder,
} = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();

const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite);
//state
const [isDragging, setIsDragging] = useState(false);
const [isMenuActive, setIsMenuActive] = useState(false);
const [closestEdge, setClosestEdge] = useState<string | null>(null);
const [isDraggedOver, setIsDraggedOver] = useState(false);

//ref
const elementRef = useRef<HTMLDivElement>(null);
const actionSectionRef = useRef<HTMLDivElement | null>(null);

const handleQuickAction = (value: boolean) => setIsMenuActive(value);

const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite);

// drag and drop
useEffect(() => {
const element = elementRef.current;

if (!element) return;
const initialData = { id: favorite.id, type: favorite.parent ? 'CHILD' : 'NON_PARENT' };

return combine(
draggable({
element,
dragHandle: elementRef.current,
canDrag: () => true,
getInitialData: () => ({ id: favorite.id, type: "CHILD" }),
getInitialData: () => initialData,
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
onDrop: (data) => {
setIsDraggedOver(false);
setIsDragging(false);
if (!data.location.current.dropTargets[0]) return;
const destinationData = data.location.current.dropTargets[0].data;

if (favorite.id && destinationData) {
const edge = extractClosestEdge(destinationData) || undefined;
const sequence = Math.round(
getDestinationStateSequence(favoriteMap, destinationData.id as string, edge) || 0
);
handleReorder(favorite.id, sequence);
}
},
onGenerateDragPreview: ({ nativeSetDragImage }) => {
setCustomNativeDragPreview({
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(
<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
<FavoriteItemTitle
href={itemLink}
icon={itemIcon}
title={itemTitle}
isSidebarCollapsed={!!sidebarCollapsed}
/>
</div>
);
return () => root.unmount();
},
nativeSetDragImage,
});
Comment on lines +83 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add cleanup for drag preview root.

The current implementation of the drag preview could potentially cause memory leaks if the component unmounts during a drag operation. Consider wrapping the root creation in a try-catch block and ensuring cleanup.

 onGenerateDragPreview: ({ nativeSetDragImage }) => {
+  let root: Root | null = null;
   setCustomNativeDragPreview({
     getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
     render: ({ container }) => {
-      const root = createRoot(container);
+      try {
+        root = createRoot(container);
+        root.render(
-      root.render(
         <div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
           <FavoriteItemTitle
             href={itemLink}
             icon={itemIcon}
             title={itemTitle}
             isSidebarCollapsed={!!sidebarCollapsed}
           />
         </div>
       );
-      return () => root.unmount();
+      return () => {
+        root?.unmount();
+        root = null;
+      };
+      } catch (error) {
+        console.error('Failed to create drag preview:', error);
+        return () => {};
+      }
     },
     nativeSetDragImage,
   });
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onGenerateDragPreview: ({ nativeSetDragImage }) => {
setCustomNativeDragPreview({
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(
<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
<FavoriteItemTitle
href={itemLink}
icon={itemIcon}
title={itemTitle}
isSidebarCollapsed={!!sidebarCollapsed}
/>
</div>
);
return () => root.unmount();
},
nativeSetDragImage,
});
onGenerateDragPreview: ({ nativeSetDragImage }) => {
let root: Root | null = null;
setCustomNativeDragPreview({
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
try {
root = createRoot(container);
root.render(
<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
<FavoriteItemTitle
href={itemLink}
icon={itemIcon}
title={itemTitle}
isSidebarCollapsed={!!sidebarCollapsed}
/>
</div>
);
return () => {
root?.unmount();
root = null;
};
} catch (error) {
console.error('Failed to create drag preview:', error);
return () => {};
}
},
nativeSetDragImage,
});

},
}),
dropTargetForElements({
element,
onDragStart: () => {
setIsDragging(true);
},
onDragEnter: () => {
getData: ({ input, element }) =>
attachClosestEdge(initialData, {
input,
element,
allowedEdges: ["top", "bottom"],
}),
onDragEnter: (args) => {
setIsDragging(true);
setIsDraggedOver(true);
setClosestEdge(extractClosestEdge(args.self.data));
},
onDragLeave: () => {
setIsDragging(false);
setIsDraggedOver(false);
setClosestEdge(null);
},
onDrop: ({ source }) => {
onDrop: ({ self, source }) => {
setIsDragging(false);
const sourceId = source?.data?.id as string | undefined;
if (!sourceId || !favoriteMap[sourceId].parent) return;
handleRemoveFromFavoritesFolder(sourceId);
setIsDraggedOver(false);
const sourceId = source.data?.id as string | undefined;
const destinationType = self.data?.type as string | undefined;
const sourceType = source.data?.type as string | undefined;

if(!sourceId) return;

if(destinationType === 'NON_PARENT'){
handleRemoveFromFavoritesFolder(sourceId)
}
},
})
);
Expand All @@ -90,6 +156,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
return (
<>
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive} sidebarCollapsed={sidebarCollapsed}>
<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} classNames="absolute top-0" />
{!sidebarCollapsed && <FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />}
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} isSidebarCollapsed={!!sidebarCollapsed} />
{!sidebarCollapsed && (
Expand All @@ -101,6 +168,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
handleRemoveFromFavorites={handleRemoveFromFavorites}
/>
)}
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} classNames="absolute bottom-0" />
</FavoriteItemWrapper>
</>
);
Expand Down
Loading