-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[WEB-2774] Chore: re-ordering functionality for entities in favorites. #6078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f641ec
12743b5
0a6cb1b
23b30d0
0bec7a1
a612ec7
b2a58a3
1b124c6
d1cc0e0
9535a4a
797a756
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||
|
|
||||||
| // 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 ]); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add The Apply this diff to update the dependency array: - }, [isDragging, favorite.id ]);
+ }, [isDragging, favorite.id, isLastChild ]);📝 Committable suggestion
Suggested change
|
||||||
|
|
||||||
|
|
||||||
| useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); | ||||||
|
|
||||||
|
|
@@ -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", | ||||||
|
|
@@ -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> | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling to getGroupedFavorites call
The fetch operation should include error handling to manage potential failures gracefully.
Apply this diff:
Also applies to: 58-58