diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 1258aa608..db2a73d4c 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1154,7 +1154,7 @@ class CycleFavoriteViewSet(BaseViewSet): workspace__slug=slug, entity_identifier=cycle_id, ) - cycle_favorite.delete() + cycle_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 4d1203f07..c6861fe4b 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -840,7 +840,7 @@ class ModuleFavoriteViewSet(BaseViewSet): entity_type="module", entity_identifier=module_id, ) - module_favorite.delete() + module_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 2dafe4d5d..88c331727 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -386,7 +386,7 @@ class PageFavoriteViewSet(BaseViewSet): entity_identifier=pk, entity_type="page", ) - page_favorite.delete() + page_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index bca8236a9..22181dacf 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -599,7 +599,7 @@ class ProjectFavoritesViewSet(BaseViewSet): user=request.user, workspace__slug=slug, ) - project_favorite.delete() + project_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 236a051aa..988108831 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -474,5 +474,5 @@ class IssueViewFavoriteViewSet(BaseViewSet): entity_type="view", entity_identifier=view_id, ) - view_favorite.delete() + view_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/packages/types/src/favorite/favorite.d.ts b/packages/types/src/favorite/favorite.d.ts index ae7bf2da8..154d0e266 100644 --- a/packages/types/src/favorite/favorite.d.ts +++ b/packages/types/src/favorite/favorite.d.ts @@ -1,14 +1,15 @@ export type IFavorite = { - id: string; - name: string; - entity_type: string; - entity_data: { - name: string; - }; - is_folder: boolean; - sort_order: number; - parent: string | null; - entity_identifier?: string | null; - children: IFavorite[]; - project_id: string | null; + id: string; + name: string; + entity_type: string; + entity_data: { + name: string; + }; + is_folder: boolean; + sort_order: number; + parent: string | null; + entity_identifier?: string | null; + children: IFavorite[]; + project_id: string | null; + sequence: number; }; diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index ea901333a..2806faede 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -66,25 +66,23 @@ export const AppSidebar: FC = observer(() => { "opacity-0": !sidebarCollapsed, })} /> - -
- -
- -
- + > + + + +
+ + + + diff --git a/web/core/components/pages/list/block-item-action.tsx b/web/core/components/pages/list/block-item-action.tsx index 1dd93c062..83f5e3941 100644 --- a/web/core/components/pages/list/block-item-action.tsx +++ b/web/core/components/pages/list/block-item-action.tsx @@ -26,7 +26,7 @@ export const BlockItemAction: FC = observer((props) => { const page = usePage(pageId); const { getUserDetails } = useMember(); // derived values - const { access, created_at, is_favorite, owned_by, addToFavorites, removeFromFavorites } = page; + const { access, created_at, is_favorite, owned_by, addToFavorites, removePageFromFavorites } = page; // derived values const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined; @@ -34,7 +34,7 @@ export const BlockItemAction: FC = observer((props) => { // handlers const handleFavorites = () => { if (is_favorite) - removeFromFavorites().then(() => + removePageFromFavorites().then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 3096ae709..ce23b73ce 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -2,14 +2,15 @@ import { useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; -import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import { useParams } from "next/navigation"; -import { PenSquare, Star, MoreHorizontal, ChevronRight } from "lucide-react"; +import { PenSquare, Star, MoreHorizontal, ChevronRight, GripVertical } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // ui import { IFavorite } from "@plane/types"; -import { CustomMenu, Tooltip, DropIndicator, setToast, TOAST_TYPE, FavoriteFolderIcon } from "@plane/ui"; +import { CustomMenu, Tooltip, DropIndicator, setToast, TOAST_TYPE, FavoriteFolderIcon, DragHandle } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -20,6 +21,7 @@ import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; // constants import { FavoriteItem } from "./favorite-item"; +import { getDestinationStateSequence } from "./favorites.helpers"; import { NewFavoriteFolder } from "./new-fav-folder"; type Props = { @@ -29,18 +31,20 @@ type Props = { }; export const FavoriteFolder: React.FC = (props) => { - const { isLastChild, favorite, handleRemoveFromFavorites } = props; + const { favorite, handleRemoveFromFavorites } = props; // store hooks const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { isMobile } = usePlatformOS(); - const { moveFavorite, getGroupedFavorites } = useFavorite(); + const { moveFavorite, getGroupedFavorites, favoriteMap, moveFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); // states const [isMenuActive, setIsMenuActive] = useState(false); const [isDragging, setIsDragging] = useState(false); - const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); const [folderToRename, setFolderToRename] = useState(null); + const [isDraggedOver, setIsDraggedOver] = useState(false); + const [closestEdge, setClosestEdge] = useState(null); + // refs const actionSectionRef = useRef(null); const elementRef = useRef(null); @@ -51,16 +55,14 @@ export const FavoriteFolder: React.FC = (props) => { moveFavorite(workspaceSlug.toString(), source, { parent: destination, }) - .then((res) => { - console.log(res, "res"); + .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Favorite moved successfully.", }); }) - .catch((err) => { - console.log(err, "err"); + .catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", @@ -69,33 +71,81 @@ export const FavoriteFolder: React.FC = (props) => { }); }; + const handleOnDropFolder = (payload: Partial) => { + 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 }; 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: getDestinationStateSequence(favoriteMap, destinationData.id as string, edge), + }; + + handleOnDropFolder(payload); + } + }, // canDrag: () => isDraggable, + }), dropTargetForElements({ element, - getData: () => ({ type: "PARENT", id: favorite.id }), - 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); }, onDragStart: () => { setIsDragging(true); }, onDrop: ({ self, source }) => { - setInstruction(undefined); setIsDragging(false); + setIsDraggedOver(false); const sourceId = source?.data?.id as string | undefined; const destinationId = self?.data?.id as string | undefined; if (sourceId === destinationId) return; if (!sourceId || !destinationId) return; - + if (favoriteMap[sourceId].parent === destinationId) return; handleOnDrop(sourceId, destinationId); }, }) @@ -122,7 +172,8 @@ export const FavoriteFolder: React.FC = (props) => { "bg-custom-sidebar-background-80 opacity-60": isDragging, })} > - + {/* draggable drop top indicator */} +
= (props) => { } )} > + {/* draggable indicator */} + +
+ +
+ {isSidebarCollapsed ? (
= (props) => { "justify-center": isSidebarCollapsed, })} > + + +
@@ -238,7 +317,8 @@ export const FavoriteFolder: React.FC = (props) => { )} - {isLastChild && } + {/* draggable drop bottom indicator */} + {" "}
)} diff --git a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx index 02467a5ea..cf326d699 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx @@ -45,7 +45,7 @@ export const FavoriteItem = observer( const actionSectionRef = useRef(null); const getIcon = () => { - const className = `flex-shrink-0 size-4 stroke-[1.5]`; + const className = `flex-shrink-0 size-4 stroke-[1.5] m-auto`; switch (favorite.entity_type) { case "page": @@ -92,7 +92,7 @@ export const FavoriteItem = observer( return combine( draggable({ element, - dragHandle: element, + // dragHandle: element, canDrag: () => true, getInitialData: () => ({ id: favorite.id, type: "CHILD" }), onDragStart: () => { @@ -144,32 +144,34 @@ export const FavoriteItem = observer( )} - setIsMenuActive(!isMenuActive)} - > - - - } - className={cn( - "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", - { - "opacity-100 pointer-events-auto": isMenuActive, + {!sidebarCollapsed && ( + setIsMenuActive(!isMenuActive)} + > + + } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - handleRemoveFromFavorites(favorite)}> - - - Remove from favorites - - - + className={cn( + "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + handleRemoveFromFavorites(favorite)}> + + + Remove from favorites + + + + )}
diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index ced7de0f0..002f34b0b 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -1,6 +1,9 @@ "use client"; import React, { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { orderBy } from "lodash"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ChevronRight, FolderPlus } from "lucide-react"; @@ -21,14 +24,16 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { FavoriteFolder } from "./favorite-folder"; import { FavoriteItem } from "./favorite-item"; import { NewFavoriteFolder } from "./new-fav-folder"; + export const SidebarFavoritesMenu = observer(() => { //state const [createNewFolder, setCreateNewFolder] = useState(null); - const [isScrolled, setIsScrolled] = useState(false); // scroll animation state + + const [isDragging, setIsDragging] = useState(false); // store hooks const { sidebarCollapsed } = useAppTheme(); - const { favoriteIds, favoriteMap, deleteFavorite } = useFavorite(); + const { favoriteIds, favoriteMap, deleteFavorite, removeFromFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); @@ -39,6 +44,7 @@ export const SidebarFavoritesMenu = observer(() => { const isFavoriteMenuOpen = !!storedValue; // refs const containerRef = useRef(null); + const elementRef = useRef(null); const handleRemoveFromFavorites = (favorite: IFavorite) => { deleteFavorite(workspaceSlug.toString(), favorite.id) @@ -57,43 +63,72 @@ export const SidebarFavoritesMenu = observer(() => { }); }); }; + const handleRemoveFromFavoritesFolder = (favoriteId: string) => { + removeFromFavoriteFolder(workspaceSlug.toString(), favoriteId, { + id: favoriteId, + parent: null, + }) + .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.", + }); + }); + }; useEffect(() => { if (sidebarCollapsed) toggleFavoriteMenu(true); }, [sidebarCollapsed, toggleFavoriteMenu]); - /** - * Implementing scroll animation styles based on the scroll length of the container - */ useEffect(() => { - const handleScroll = () => { - if (containerRef.current) { - const scrollTop = containerRef.current.scrollTop; - setIsScrolled(scrollTop > 0); - } - }; - const currentContainerRef = containerRef.current; - if (currentContainerRef) { - currentContainerRef.addEventListener("scroll", handleScroll); - } - return () => { - if (currentContainerRef) { - currentContainerRef.removeEventListener("scroll", handleScroll); - } - }; - }, [containerRef]); + const element = elementRef.current; + + if (!element) return; + + return combine( + dropTargetForElements({ + element, + onDragEnter: () => { + setIsDragging(true); + }, + onDragLeave: () => { + setIsDragging(false); + }, + onDragStart: () => { + setIsDragging(true); + }, + onDrop: ({ source }) => { + setIsDragging(false); + const sourceId = source?.data?.id as string | undefined; + console.log({ sourceId }); + if (!sourceId || !favoriteMap[sourceId].parent) return; + handleRemoveFromFavoritesFolder(sourceId); + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef.current, isDragging]); + return ( -
- + <> + {!sidebarCollapsed && ( toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start"> MY FAVORITES @@ -133,27 +168,25 @@ export const SidebarFavoritesMenu = observer(() => { static > {createNewFolder && } - {favoriteIds - .filter((id) => !favoriteMap[id].parent) - .map((id, index) => ( + {orderBy(Object.values(favoriteMap), "sequence", "desc") + .filter((fav) => !fav.parent) + .map((fav, index) => ( - {favoriteMap[id].is_folder ? ( + {fav.is_folder ? ( ) : ( - + )} ))} @@ -161,6 +194,12 @@ export const SidebarFavoritesMenu = observer(() => { )} -
+ +
+ ); }); diff --git a/web/core/components/workspace/sidebar/favorites/favorites.helpers.ts b/web/core/components/workspace/sidebar/favorites/favorites.helpers.ts new file mode 100644 index 000000000..3551cd105 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorites.helpers.ts @@ -0,0 +1,35 @@ +import orderBy from "lodash/orderBy"; +import { IFavorite } from "@plane/types"; + +export const getDestinationStateSequence = ( + favoriteMap: Record, + destinationId: string, + edge: string | undefined +) => { + const defaultSequence = 65535; + if (!edge) return defaultSequence; + + const favoriteIds = orderBy(Object.values(favoriteMap), "sequence", "desc") + .filter((fav: IFavorite) => !fav.parent) + .map((fav: IFavorite) => fav.id); + const destinationStateIndex = favoriteIds.findIndex((id) => id === destinationId); + const destinationStateSequence = favoriteMap[destinationId]?.sequence || undefined; + + if (!destinationStateSequence) return defaultSequence; + + if (edge === "top") { + const prevStateSequence = favoriteMap[favoriteIds[destinationStateIndex - 1]]?.sequence || undefined; + + if (prevStateSequence === undefined) { + return destinationStateSequence + defaultSequence; + } + return (destinationStateSequence + prevStateSequence) / 2; + } else if (edge === "bottom") { + const nextStateSequence = favoriteMap[favoriteIds[destinationStateIndex + 1]]?.sequence || undefined; + + if (nextStateSequence === undefined) { + return destinationStateSequence - defaultSequence; + } + return (destinationStateSequence + nextStateSequence) / 2; + } +}; diff --git a/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx index e17a4c787..7f0a47d88 100644 --- a/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from "react"; +import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { FavoriteFolderIcon, Input, setToast, TOAST_TYPE } from "@plane/ui"; @@ -18,10 +19,10 @@ type TProps = { defaultName?: string; favoriteId?: string; }; -export const NewFavoriteFolder = (props: TProps) => { +export const NewFavoriteFolder = observer((props: TProps) => { const { setCreateNewFolder, actionType, defaultName, favoriteId } = props; const { workspaceSlug } = useParams(); - const { addFavorite, updateFavorite } = useFavorite(); + const { addFavorite, updateFavorite, existingFolders } = useFavorite(); // ref const ref = useRef(null); @@ -35,6 +36,12 @@ export const NewFavoriteFolder = (props: TProps) => { }); const handleAddNewFolder: SubmitHandler = (formData) => { + if (existingFolders.includes(formData.name)) + return setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Folder already exists", + }); formData = { entity_type: "folder", is_folder: true, @@ -63,6 +70,12 @@ export const NewFavoriteFolder = (props: TProps) => { const handleRenameFolder: SubmitHandler = (formData) => { if (!favoriteId) return; + if (existingFolders.includes(formData.name)) + return setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Folder already exists", + }); const payload = { name: formData.name, }; @@ -86,14 +99,15 @@ export const NewFavoriteFolder = (props: TProps) => { }); return (
- +
} + rules={{ required: true }} + render={({ field }) => } />
); -}; +}); diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index f40240273..38e50c556 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -367,22 +367,19 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { customButtonClassName="grid place-items-center" placement="bottom-start" > - {!project.is_favorite && ( - - - - Add to favorites - - - )} - {project.is_favorite && ( - - - - Remove from favorites - - - )} + + + + {project.is_favorite ? "Remove from favorites" : "Add to favorites"} + + + {/* publish project settings */} {isAdmin && ( setPublishModal(true)}> diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index 5515f0105..8c3a60dad 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -140,107 +140,105 @@ export const SidebarProjectsList: FC = observer(() => { )}
<> - <> -
+ toggleListDisclosure(!isAllProjectsListOpen)} > - toggleListDisclosure(!isAllProjectsListOpen)} - > - - <> - {isCollapsed ? ( - - ) : ( - YOUR PROJECTS - )} - - - - {!isCollapsed && ( -
- {isAuthorizedUser && ( - - - + + <> + {isCollapsed ? ( + + ) : ( + YOUR PROJECTS )} - toggleListDisclosure(!isAllProjectsListOpen)} - > - - -
- )} -
- - {isAllProjectsListOpen && ( - + + + {!isCollapsed && ( +
+ {isAuthorizedUser && ( + + + + )} + toggleListDisclosure(!isAllProjectsListOpen)} > - {joinedProjects.map((projectId, index) => ( - handleCopyText(projectId)} - projectListType={"JOINED"} - disableDrag={false} - disableDrop={false} - isLastChild={index === joinedProjects.length - 1} - handleOnProjectDrop={handleOnProjectDrop} - /> - ))} - - )} - - + + +
+ )} +
+ + {isAllProjectsListOpen && ( + + {joinedProjects.map((projectId, index) => ( + handleCopyText(projectId)} + projectListType={"JOINED"} + disableDrag={false} + disableDrop={false} + isLastChild={index === joinedProjects.length - 1} + handleOnProjectDrop={handleOnProjectDrop} + /> + ))} + + )} + diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index edf5617a4..67e569784 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -66,7 +66,7 @@ export const SidebarWorkspaceMenu = observer(() => { {!sidebarCollapsed && ( toggleWorkspaceMenu(!isWorkspaceMenuOpen)} > WORKSPACE diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index f6a56e450..796626059 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -1,9 +1,10 @@ import { uniqBy } from "lodash"; import set from "lodash/set"; -import { action, observable, makeObservable, runInAction } from "mobx"; +import { action, observable, makeObservable, runInAction, computed } from "mobx"; import { v4 as uuidv4 } from "uuid"; import { IFavorite } from "@plane/types"; import { FavoriteService } from "@/services/favorite"; +import { CoreRootStore } from "./root.store"; export interface IFavoriteStore { // observables @@ -16,6 +17,7 @@ export interface IFavoriteStore { [entityId: string]: IFavorite; }; // computed actions + existingFolders: string[]; // actions fetchFavorite: (workspaceSlug: string) => Promise; // CRUD actions @@ -25,6 +27,8 @@ export interface IFavoriteStore { getGroupedFavorites: (workspaceSlug: string, favoriteId: string) => Promise; moveFavorite: (workspaceSlug: string, favoriteId: string, data: Partial) => Promise; removeFavoriteEntity: (workspaceSlug: string, entityId: string) => Promise; + moveFavoriteFolder: (workspaceSlug: string, favoriteId: string, data: Partial) => Promise; + removeFromFavoriteFolder: (workspaceSlug: string, favoriteId: string, data: Partial) => Promise; } export class FavoriteStore implements IFavoriteStore { @@ -38,13 +42,20 @@ export class FavoriteStore implements IFavoriteStore { } = {}; // service favoriteService; + viewStore; + projectStore; + pageStore; + cycleStore; + moduleStore; - constructor() { + constructor(_rootStore: CoreRootStore) { makeObservable(this, { // observable favoriteMap: observable, entityMap: observable, favoriteIds: observable, + //computed + existingFolders: computed, // action fetchFavorite: action, // CRUD actions @@ -52,8 +63,20 @@ export class FavoriteStore implements IFavoriteStore { getGroupedFavorites: action, moveFavorite: action, removeFavoriteEntity: action, + moveFavoriteFolder: action, + removeFavoriteEntityFromStore: action, + removeFromFavoriteFolder: action, }); this.favoriteService = new FavoriteService(); + this.viewStore = _rootStore.projectView; + this.projectStore = _rootStore.projectRoot.project; + this.moduleStore = _rootStore.module; + this.cycleStore = _rootStore.cycle; + this.pageStore = _rootStore.projectPages; + } + + get existingFolders() { + return Object.values(this.favoriteMap).map((fav) => fav.name); } /** @@ -147,6 +170,65 @@ export class FavoriteStore implements IFavoriteStore { } }; + moveFavoriteFolder = async (workspaceSlug: string, favoriteId: string, data: Partial) => { + const initialSequence = this.favoriteMap[favoriteId].sequence; + try { + runInAction(() => { + set(this.favoriteMap, [favoriteId, "sequence"], data.sequence); + }); + console.log(JSON.parse(JSON.stringify(this.favoriteMap)), "getDestinationStateSequence"); + + await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); + } catch (error) { + runInAction(() => { + set(this.favoriteMap, [favoriteId, "sequence"], initialSequence); + console.error("Failed to move favorite folder"); + throw error; + }); + } + }; + + removeFromFavoriteFolder = async (workspaceSlug: string, favoriteId: string, data: Partial) => { + try { + await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); + runInAction(() => { + const parent = this.favoriteMap[favoriteId].parent; + + //remove parent + set(this.favoriteMap, [favoriteId, "parent"], null); + + //remove children from parent + if (parent) { + set( + this.favoriteMap, + [parent, "children"], + this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) + ); + } + }); + } catch (error) { + console.error("Failed to move favorite"); + throw error; + } + }; + + removeFavoriteEntityFromStore = (entity_identifier: string, entity_type: string) => { + switch (entity_type) { + case "view": + return (this.viewStore.viewMap[entity_identifier].is_favorite = false); + case "module": + return (this.moduleStore.moduleMap[entity_identifier].is_favorite = false); + case "page": + return (this.pageStore.data[entity_identifier].is_favorite = false); + case "cycle": + return (this.cycleStore.cycleMap[entity_identifier].is_favorite = false); + case "project": + return (this.projectStore.projectMap[entity_identifier].is_favorite = false); + default: + return; + } + }; + /** * Deletes a favorite from the workspace and updates the store * @param workspaceSlug @@ -158,6 +240,10 @@ export class FavoriteStore implements IFavoriteStore { await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); runInAction(() => { const parent = this.favoriteMap[favoriteId].parent; + const children = this.favoriteMap[favoriteId].children; + const entity_identifier = this.favoriteMap[favoriteId].entity_identifier; + entity_identifier && + this.removeFavoriteEntityFromStore(entity_identifier, this.favoriteMap[favoriteId].entity_type); if (parent) { set( this.favoriteMap, @@ -165,7 +251,16 @@ export class FavoriteStore implements IFavoriteStore { this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) ); } + if (children) { + children.forEach((child) => { + console.log(child.entity_type); + if (!child.entity_identifier) return; + this.removeFavoriteEntityFromStore(child.entity_identifier, child.entity_type); + }); + } delete this.favoriteMap[favoriteId]; + entity_identifier && delete this.entityMap[entity_identifier]; + this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); } catch (error) { diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 6a7ba74c3..507259de7 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -41,7 +41,7 @@ export interface IPage extends TPage { restore: () => Promise; updatePageLogo: (logo_props: TLogoProps) => Promise; addToFavorites: () => Promise; - removeFromFavorites: () => Promise; + removePageFromFavorites: () => Promise; } export class Page implements IPage { @@ -146,7 +146,7 @@ export class Page implements IPage { restore: action, updatePageLogo: action, addToFavorites: action, - removeFromFavorites: action, + removePageFromFavorites: action, }); this.pageService = new ProjectPageService(); @@ -497,7 +497,7 @@ export class Page implements IPage { /** * @description remove the page from favorites */ - removeFromFavorites = async () => { + removePageFromFavorites = async () => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index f4d6d3e1a..b4ced1d07 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -285,6 +285,7 @@ export class ProjectStore implements IProjectStore { project_id: projectId, entity_data: { name: this.projectMap[projectId].name || "" }, }); + await this.fetchProjects(workspaceSlug); return response; } catch (error) { console.log("Failed to add project to favorite"); diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 1bab15b34..709076ceb 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -80,7 +80,7 @@ export class CoreRootStore { this.projectPages = new ProjectPageStore(this); this.projectEstimate = new ProjectEstimateStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); - this.favorite = new FavoriteStore(); + this.favorite = new FavoriteStore(this); } resetOnSignOut() {