mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
[WEB-1907] Fix: favorites (#5292)
* chore: workspace user favorites * chore: added project id in entity type * chore: removed the extra key * chore: removed the project member filter * chore: updated the project permission layer * chore: updated the workspace group favorite filter * fix: project favorite toggle * chore: Fav feature * fix: build errors + added navigation * fix: added remove entity icon * fix: nomenclature * chore: hard delete favorites * fix: review changes * fix: added optimistic addition to the store * chore: user favorite hard delete * fix: linting fixed * fix: favorite bugs * fix: ts bugs --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
25
packages/types/src/favorite/favorite.d.ts
vendored
25
packages/types/src/favorite/favorite.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
@@ -66,25 +66,23 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarUserMenu />
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
<div
|
||||
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2.5", {
|
||||
"vertical-scrollbar": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarWorkspaceMenu />
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarFavoritesMenu />
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarProjectsList />
|
||||
>
|
||||
<SidebarUserMenu />
|
||||
|
||||
<SidebarWorkspaceMenu />
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarFavoritesMenu />
|
||||
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
<SidebarHelpSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const BlockItemAction: FC<Props> = 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<Props> = observer((props) => {
|
||||
// handlers
|
||||
const handleFavorites = () => {
|
||||
if (is_favorite)
|
||||
removeFromFavorites().then(() =>
|
||||
removePageFromFavorites().then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
|
||||
@@ -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> = (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<string | boolean | null>(null);
|
||||
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
||||
const [closestEdge, setClosestEdge] = useState<string | null>(null);
|
||||
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -51,16 +55,14 @@ export const FavoriteFolder: React.FC<Props> = (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> = (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;
|
||||
|
||||
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> = (props) => {
|
||||
"bg-custom-sidebar-background-80 opacity-60": isDragging,
|
||||
})}
|
||||
>
|
||||
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
|
||||
{/* draggable drop top indicator */}
|
||||
<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />
|
||||
<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",
|
||||
@@ -132,6 +183,12 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
{/* draggable indicator */}
|
||||
|
||||
<div className="flex-shrink-0 w-3 h-3 rounded-sm absolute left-0 hidden group-hover:flex justify-center items-center transition-colors bg-custom-background-90 cursor-pointer text-custom-text-200 hover:text-custom-text-100">
|
||||
<GripVertical className="w-3 h-3" />
|
||||
</div>
|
||||
|
||||
{isSidebarCollapsed ? (
|
||||
<div
|
||||
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
|
||||
@@ -160,6 +217,28 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
"justify-center": isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
favorite.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"
|
||||
}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
|
||||
{
|
||||
"cursor-not-allowed opacity-60": favorite.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
"!hidden": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<FavoriteFolderIcon />
|
||||
</div>
|
||||
@@ -238,7 +317,8 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
)}
|
||||
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
|
||||
{/* draggable drop bottom indicator */}
|
||||
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />{" "}
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const FavoriteItem = observer(
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(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(
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
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 && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
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"
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</div>
|
||||
|
||||
@@ -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<boolean | string | null>(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<HTMLDivElement | null>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("-mr-3 -ml-4 pl-4", {
|
||||
"border-t border-custom-sidebar-border-300": isScrolled,
|
||||
"vertical-scrollbar h-full !overflow-y-scroll scrollbar-sm": isFavoriteMenuOpen,
|
||||
})}
|
||||
>
|
||||
<Disclosure as="div" defaultOpen>
|
||||
<>
|
||||
<Disclosure as="div" defaultOpen ref={containerRef}>
|
||||
{!sidebarCollapsed && (
|
||||
<Disclosure.Button
|
||||
ref={elementRef}
|
||||
as="button"
|
||||
className="group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
||||
className={cn(
|
||||
"sticky top-0 bg-custom-sidebar-background-100 z-10 group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold",
|
||||
{
|
||||
"bg-custom-sidebar-background-80 opacity-60": isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start">
|
||||
MY FAVORITES
|
||||
@@ -133,27 +168,25 @@ export const SidebarFavoritesMenu = observer(() => {
|
||||
static
|
||||
>
|
||||
{createNewFolder && <NewFavoriteFolder setCreateNewFolder={setCreateNewFolder} actionType="create" />}
|
||||
{favoriteIds
|
||||
.filter((id) => !favoriteMap[id].parent)
|
||||
.map((id, index) => (
|
||||
{orderBy(Object.values(favoriteMap), "sequence", "desc")
|
||||
.filter((fav) => !fav.parent)
|
||||
.map((fav, index) => (
|
||||
<Tooltip
|
||||
key={favoriteMap[id].id}
|
||||
tooltipContent={
|
||||
favoriteMap[id].entity_data ? favoriteMap[id].entity_data.name : favoriteMap[id].name
|
||||
}
|
||||
key={fav.id}
|
||||
tooltipContent={fav.entity_data ? fav.entity_data.name : fav.name}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
{favoriteMap[id].is_folder ? (
|
||||
{fav.is_folder ? (
|
||||
<FavoriteFolder
|
||||
favorite={favoriteMap[id]}
|
||||
favorite={fav}
|
||||
isLastChild={index === favoriteIds.length - 1}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
/>
|
||||
) : (
|
||||
<FavoriteItem favorite={favoriteMap[id]} handleRemoveFromFavorites={handleRemoveFromFavorites} />
|
||||
<FavoriteItem favorite={fav} handleRemoveFromFavorites={handleRemoveFromFavorites} />
|
||||
)}
|
||||
</Tooltip>
|
||||
))}
|
||||
@@ -161,6 +194,12 @@ export const SidebarFavoritesMenu = observer(() => {
|
||||
)}
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
</div>
|
||||
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed || favoriteIds.length === 0,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { IFavorite } from "@plane/types";
|
||||
|
||||
export const getDestinationStateSequence = (
|
||||
favoriteMap: Record<string, IFavorite>,
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -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<TForm> = (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<TForm> = (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 (
|
||||
<div className="flex items-center gap-1.5 py-[1px] px-2" ref={ref}>
|
||||
<FavoriteFolderIcon />
|
||||
<FavoriteFolderIcon className="w-[16px]" />
|
||||
<form onSubmit={handleSubmit(actionType === "create" ? handleAddNewFolder : handleRenameFolder)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => <Input placeholder="New folder" {...field} />}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => <Input className="w-full" placeholder="New folder" {...field} />}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -367,22 +367,19 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{!project.is_favorite && (
|
||||
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Add to favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{project.is_favorite && (
|
||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star
|
||||
className={cn("h-3.5 w-3.5 ", {
|
||||
"fill-yellow-500 stroke-yellow-500": project.is_favorite,
|
||||
})}
|
||||
/>
|
||||
<span>{project.is_favorite ? "Remove from favorites" : "Add to favorites"}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
|
||||
{/* publish project settings */}
|
||||
{isAdmin && (
|
||||
<CustomMenu.MenuItem onClick={() => setPublishModal(true)}>
|
||||
|
||||
@@ -140,107 +140,105 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("vertical-scrollbar h-full !overflow-y-scroll scrollbar-sm -mr-3 -ml-4 pl-4", {
|
||||
className={cn({
|
||||
"border-t border-custom-sidebar-border-300": isScrolled,
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
|
||||
<>
|
||||
<div
|
||||
<div
|
||||
className={cn(
|
||||
"group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
|
||||
isCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
|
||||
"group w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
|
||||
{
|
||||
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
|
||||
isCollapsed,
|
||||
"!text-center w-8 px-2 py-1.5 justify-center": isCollapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"group w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
|
||||
{
|
||||
"!text-center w-8 px-2 py-1.5 justify-center": isCollapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
>
|
||||
<Tooltip tooltipHeading="YOUR PROJECTS" tooltipContent="" position="right" disabled={!isCollapsed}>
|
||||
<>
|
||||
{isCollapsed ? (
|
||||
<Briefcase className="flex-shrink-0 size-3" />
|
||||
) : (
|
||||
<span className="text-xs font-semibold">YOUR PROJECTS</span>
|
||||
)}
|
||||
</>
|
||||
</Tooltip>
|
||||
</Disclosure.Button>
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
{isAuthorizedUser && (
|
||||
<Tooltip tooltipHeading="Create project" tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => {
|
||||
setTrackElement(`APP_SIDEBAR_JOINED_BLOCK`);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipHeading="YOUR PROJECTS" tooltipContent="" position="right" disabled={!isCollapsed}>
|
||||
<>
|
||||
{isCollapsed ? (
|
||||
<Briefcase className="flex-shrink-0 size-3" />
|
||||
) : (
|
||||
<span className="text-xs font-semibold">YOUR PROJECTS</span>
|
||||
)}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-4 transition-all", {
|
||||
"rotate-90": isAllProjectsListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Transition
|
||||
show={isAllProjectsListOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isAllProjectsListOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("space-y-1", {
|
||||
"space-y-0 ml-0": isCollapsed,
|
||||
})}
|
||||
static
|
||||
</>
|
||||
</Tooltip>
|
||||
</Disclosure.Button>
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
{isAuthorizedUser && (
|
||||
<Tooltip tooltipHeading="Create project" tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => {
|
||||
setTrackElement(`APP_SIDEBAR_JOINED_BLOCK`);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
>
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<SidebarProjectsListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
projectListType={"JOINED"}
|
||||
disableDrag={false}
|
||||
disableDrop={false}
|
||||
isLastChild={index === joinedProjects.length - 1}
|
||||
handleOnProjectDrop={handleOnProjectDrop}
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Transition>
|
||||
</>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-4 transition-all", {
|
||||
"rotate-90": isAllProjectsListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Transition
|
||||
show={isAllProjectsListOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isAllProjectsListOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("space-y-1", {
|
||||
"space-y-0 ml-0": isCollapsed,
|
||||
})}
|
||||
static
|
||||
>
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<SidebarProjectsListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
projectListType={"JOINED"}
|
||||
disableDrag={false}
|
||||
disableDrop={false}
|
||||
isLastChild={index === joinedProjects.length - 1}
|
||||
handleOnProjectDrop={handleOnProjectDrop}
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
</>
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||
{!sidebarCollapsed && (
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
className="group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
||||
className="sticky top-0 bg-custom-sidebar-background-100 z-10 group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
||||
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
||||
>
|
||||
<span>WORKSPACE</span>
|
||||
|
||||
@@ -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<IFavorite[]>;
|
||||
// CRUD actions
|
||||
@@ -25,6 +27,8 @@ export interface IFavoriteStore {
|
||||
getGroupedFavorites: (workspaceSlug: string, favoriteId: string) => Promise<IFavorite[]>;
|
||||
moveFavorite: (workspaceSlug: string, favoriteId: string, data: Partial<IFavorite>) => Promise<void>;
|
||||
removeFavoriteEntity: (workspaceSlug: string, entityId: string) => Promise<void>;
|
||||
moveFavoriteFolder: (workspaceSlug: string, favoriteId: string, data: Partial<IFavorite>) => Promise<void>;
|
||||
removeFromFavoriteFolder: (workspaceSlug: string, favoriteId: string, data: Partial<IFavorite>) => Promise<void>;
|
||||
}
|
||||
|
||||
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<IFavorite>) => {
|
||||
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<IFavorite>) => {
|
||||
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) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface IPage extends TPage {
|
||||
restore: () => Promise<void>;
|
||||
updatePageLogo: (logo_props: TLogoProps) => Promise<void>;
|
||||
addToFavorites: () => Promise<void>;
|
||||
removeFromFavorites: () => Promise<void>;
|
||||
removePageFromFavorites: () => Promise<void>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user