Skip to content
Merged
165 changes: 67 additions & 98 deletions web/core/components/workspace/sidebar/favorites/favorite-folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,160 +2,127 @@

import { useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { DragLocationHistory, ElementDragPayload, DropTargetRecord } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
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";
import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";

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

// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
// ui
import { IFavorite } from "@plane/types";
import { CustomMenu, Tooltip, DropIndicator, setToast, TOAST_TYPE, FavoriteFolderIcon, DragHandle } from "@plane/ui";
import { IFavorite, InstructionType } from "@plane/types";
import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme } from "@/hooks/store";
import { useFavorite } from "@/hooks/store/use-favorite";
import { usePlatformOS } from "@/hooks/use-platform-os";
// constants
import { FavoriteRoot } from "./favorite-items";
import { getDestinationStateSequence } from "./favorites.helpers";
import { getCanDrop, getInstructionFromPayload } from "./favorites.helpers";
import { NewFavoriteFolder } from "./new-fav-folder";

type Props = {
isLastChild: boolean;
favorite: IFavorite;
handleRemoveFromFavorites: (favorite: IFavorite) => void;
handleRemoveFromFavoritesFolder: (favoriteId: string) => void;
handleReorder: (favoriteId: string, sequence: number) => void;
handleDrop: (self: DropTargetRecord,source: ElementDragPayload, location: DragLocationHistory) => void;
};

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

const { isMobile } = usePlatformOS();
const { moveFavorite, getGroupedFavorites, groupedFavorites, moveFavoriteFolder } = useFavorite();
const { workspaceSlug } = useParams();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [folderToRename, setFolderToRename] = useState<string | boolean | null>(null);
const [isDraggedOver, setIsDraggedOver] = useState(false);
const [closestEdge, setClosestEdge] = useState<string | null>(null);
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
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 error handling to getGroupedFavorites call

The fetch operation should include error handling to manage potential failures gracefully.

Apply this diff:

-if(!favorite.children) getGroupedFavorites(workspaceSlug.toString(), favorite.id);
+if(!favorite.children) {
+  getGroupedFavorites(workspaceSlug.toString(), favorite.id)
+    .catch((error) => {
+      setToast({
+        type: TOAST_TYPE.ERROR,
+        title: "Error!",
+        message: "Failed to fetch favorites.",
+      });
+    });
+}

Also applies to: 58-58


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

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

const handleOnDrop = (source: string, destination: string) => {
moveFavorite(workspaceSlug.toString(), source, {
parent: destination,
})
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Favorite moved successfully.",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to move favorite.",
});
});
};

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;

if (!element) return;
const initialData = { type: "PARENT", id: favorite.id, is_folder: favorite.is_folder };
const initialData = { id: favorite.id, isGroup: true, isChild: false };

return combine(
draggable({
element,
getInitialData: () => initialData,
onDragStart: () => setIsDragging(true),
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);
}
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: () => {
setIsDragging(false)
}, // canDrag: () => isDraggable,
}),
dropTargetForElements({
element,
getData: ({ input, element }) =>
attachClosestEdge(initialData, {
canDrop: ({ source }) => getCanDrop(source, favorite, false),
getData: ({ input, element }) =>{

const blockedStates: InstructionType[] = [];
if(!isLastChild){
blockedStates.push('reorder-below');
}

return attachInstruction(initialData,{
input,
element,
allowedEdges: ["top", "bottom"],
}),
onDragEnter: (args) => {
setIsDragging(true);
setIsDraggedOver(true);
args.source.data.is_folder && setClosestEdge(extractClosestEdge(args.self.data));
currentLevel: 0,
indentPerLevel: 0,
mode: isLastChild ? 'last-in-group' : 'standard',
block: blockedStates
})
},
onDragLeave: () => {
setIsDragging(false);
setIsDraggedOver(false);
setClosestEdge(null);
},
onDragStart: () => {
setIsDragging(true);
onDrag: ({source, self, location}) => {
const instruction = getInstructionFromPayload(self,source, location);
setInstruction(instruction);
},
onDrop: ({ self, source }) => {
setIsDragging(false);
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;
handleOnDrop(sourceId, destinationId);
onDragLeave: () => {
setInstruction(undefined);
},
onDrop: ({ self, source, location})=>{
setInstruction(undefined);
handleDrop(self, source,location);
}
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef.current, isDragging, favorite.id, handleOnDrop]);
}, [isDragging, favorite.id ]);
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 isLastChild to the useEffect dependency array

The isLastChild variable is used within the useEffect hook but is not included in its dependency array. Omitting it may lead to unexpected behavior if isLastChild changes.

Apply this diff to update the dependency array:

- }, [isDragging, favorite.id ]);
+ }, [isDragging, favorite.id, isLastChild ]);
📝 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
}, [isDragging, favorite.id ]);
}, [isDragging, favorite.id, isLastChild ]);



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

Expand All @@ -174,10 +141,11 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
// id={`sidebar-${projectId}-${projectListType}`}
className={cn("relative", {
"bg-custom-sidebar-background-80 opacity-60": isDragging,
"border-[2px] border-custom-primary-100" : instruction === 'make-child'
})}
>
{/* draggable drop top indicator */}
<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />
<DropIndicator isVisible={instruction === "reorder-above"}/>
<div
className={cn(
"group/project-item relative w-full px-2 py-1.5 flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90",
Expand Down Expand Up @@ -316,21 +284,22 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
"px-2": !isSidebarCollapsed,
})}
>
{uniqBy(favorite.children, "id").map((child) => (
{orderBy(favorite.children,'sequence','desc').map((child,index) => (
<FavoriteRoot
key={child.id}
workspaceSlug={workspaceSlug.toString()}
favorite={child}
isLastChild={index === favorite.children.length - 1}
parentId={favorite.id}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
favoriteMap={groupedFavorites}
handleDrop={handleDrop}
/>
))}
</Disclosure.Panel>
</Transition>
)}
{/* draggable drop bottom indicator */}
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />{" "}
{ isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}
</div>
)}
</Disclosure>
Expand Down
Loading