Skip to content

Commit

Permalink
[WEB-1907] Fix: favorites (makeplane#5292)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
gakshita and NarayanBavisetti authored Aug 4, 2024
1 parent 93e6c3b commit 34820ee
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 238 deletions.
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/cycle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ def destroy(self, request, slug, project_id, cycle_id):
workspace__slug=slug,
entity_identifier=cycle_id,
)
cycle_favorite.delete()
cycle_favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/module/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ def destroy(self, request, slug, project_id, module_id):
entity_type="module",
entity_identifier=module_id,
)
module_favorite.delete()
module_favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/page/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def destroy(self, request, slug, project_id, pk):
entity_identifier=pk,
entity_type="page",
)
page_favorite.delete()
page_favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/project/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def destroy(self, request, slug, project_id):
user=request.user,
workspace__slug=slug,
)
project_favorite.delete()
project_favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/view/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,5 +474,5 @@ def destroy(self, request, slug, project_id, view_id):
entity_type="view",
entity_identifier=view_id,
)
view_favorite.delete()
view_favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
25 changes: 13 additions & 12 deletions packages/types/src/favorite/favorite.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};
34 changes: 16 additions & 18 deletions web/app/[workspaceSlug]/(projects)/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})}
/>
<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,
<div
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2.5", {
"vertical-scrollbar": !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>
Expand Down
4 changes: 2 additions & 2 deletions web/core/components/pages/list/block-item-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ 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;

// handlers
const handleFavorites = () => {
if (is_favorite)
removeFromFavorites().then(() =>
removePageFromFavorites().then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
Expand Down
112 changes: 96 additions & 16 deletions web/core/components/workspace/sidebar/favorites/favorite-folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
Expand All @@ -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);
Expand All @@ -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!",
Expand All @@ -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);
},
})
Expand All @@ -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",
Expand All @@ -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", {
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Loading

0 comments on commit 34820ee

Please sign in to comment.