[WEB-2202] chore: user favorites mutation and code refactor (#5330)
* chore: fav item drag and drop improvement * chore: user favorite type updated * chore: user favorites helper function added * dev: favorite item common component added * dev: favorite item component added and code refactor * fix: build error * chore: code refactor * chore: code refactor * chore: code refactor
This commit is contained in:
parent
a2098ffb5e
commit
48cb0f5afc
15 changed files with 381 additions and 224 deletions
1
packages/types/src/favorite/favorite.d.ts
vendored
1
packages/types/src/favorite/favorite.d.ts
vendored
|
|
@ -15,6 +15,7 @@ export type IFavorite = {
|
|||
name: string;
|
||||
entity_type: string;
|
||||
entity_data: {
|
||||
id?: string;
|
||||
name: string;
|
||||
logo_props?: TLogoProps | undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { useFavorite } from "@/hooks/store/use-favorite";
|
|||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
import { FavoriteItem } from "./favorite-item";
|
||||
import { FavoriteRoot } from "./favorite-items";
|
||||
import { getDestinationStateSequence } from "./favorites.helpers";
|
||||
import { NewFavoriteFolder } from "./new-fav-folder";
|
||||
|
||||
|
|
@ -314,8 +314,9 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
})}
|
||||
>
|
||||
{favorite.children.map((child) => (
|
||||
<FavoriteItem
|
||||
<FavoriteRoot
|
||||
key={child.id}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
favorite={child}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
|
||||
|
|
|
|||
|
|
@ -1,220 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Briefcase, FileText, Layers, MoreHorizontal, Star } from "lucide-react";
|
||||
// ui
|
||||
import { IFavorite } from "@plane/types";
|
||||
import { ContrastIcon, CustomMenu, DiceIcon, DragHandle, FavoriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
const iconClassName = `flex-shrink-0 size-4 stroke-[1.5] m-auto`;
|
||||
const ICONS: Record<string, JSX.Element> = {
|
||||
page: <FileText className={iconClassName} />,
|
||||
project: <Briefcase className={iconClassName} />,
|
||||
view: <Layers className={iconClassName} />,
|
||||
module: <DiceIcon className={iconClassName} />,
|
||||
cycle: <ContrastIcon className={iconClassName} />,
|
||||
issue: <LayersIcon className={iconClassName} />,
|
||||
folder: <FavoriteFolderIcon className={iconClassName} />,
|
||||
};
|
||||
|
||||
export const FavoriteItem = observer(
|
||||
({
|
||||
favoriteMap,
|
||||
favorite,
|
||||
handleRemoveFromFavorites,
|
||||
handleRemoveFromFavoritesFolder,
|
||||
}: {
|
||||
favorite: IFavorite;
|
||||
favoriteMap: Record<string, IFavorite>;
|
||||
handleRemoveFromFavorites: (favorite: IFavorite) => void;
|
||||
handleRemoveFromFavoritesFolder: (favoriteId: string) => void;
|
||||
}) => {
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { isMobile } = usePlatformOS();
|
||||
//state
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// derived values
|
||||
|
||||
//ref
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const getIcon = () => (
|
||||
<>
|
||||
<div className="hidden group-hover:flex items-center justify-center size-5">
|
||||
{ICONS[favorite.entity_type] || <FileText />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center size-5 group-hover:hidden">
|
||||
{favorite.entity_data?.logo_props?.in_use ? (
|
||||
<Logo
|
||||
logo={favorite.entity_data?.logo_props}
|
||||
size={16}
|
||||
type={favorite.entity_type === "project" ? "material" : "lucide"}
|
||||
/>
|
||||
) : (
|
||||
ICONS[favorite.entity_type] || <FileText />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const getLink = () => {
|
||||
switch (favorite.entity_type) {
|
||||
case "project":
|
||||
return `/${workspaceSlug}/projects/${favorite.project_id}/issues`;
|
||||
case "cycle":
|
||||
return `/${workspaceSlug}/projects/${favorite.project_id}/cycles/${favorite.entity_identifier}`;
|
||||
case "module":
|
||||
return `/${workspaceSlug}/projects/${favorite.project_id}/modules/${favorite.entity_identifier}`;
|
||||
case "view":
|
||||
return `/${workspaceSlug}/projects/${favorite.project_id}/views/${favorite.entity_identifier}`;
|
||||
case "page":
|
||||
return `/${workspaceSlug}/projects/${favorite.project_id}/pages/${favorite.entity_identifier}`;
|
||||
default:
|
||||
return `/${workspaceSlug}`;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
// dragHandle: element,
|
||||
canDrag: () => true,
|
||||
getInitialData: () => ({ id: favorite.id, type: "CHILD" }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDragEnter: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onDrop: ({ source }) => {
|
||||
setIsDragging(false);
|
||||
const sourceId = source?.data?.id as string | undefined;
|
||||
if (!sourceId || !favoriteMap[sourceId].parent) return;
|
||||
handleRemoveFromFavoritesFolder(sourceId);
|
||||
},
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, isDragging]);
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
return (
|
||||
<>
|
||||
{sidebarCollapsed ? (
|
||||
<div ref={elementRef}>
|
||||
<Link
|
||||
href={getLink()}
|
||||
className={cn(
|
||||
"group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center justify-center size-5">{getIcon()}</span>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<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,
|
||||
}
|
||||
)}
|
||||
ref={dragHandleRef}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Link href={getLink()} className="flex items-center gap-1.5 truncate w-full">
|
||||
<div className="flex items-center justify-center size-5">{getIcon()}</div>
|
||||
<span className="text-sm leading-5 font-medium flex-1 truncate">
|
||||
{favorite.entity_data ? favorite.entity_data.name : favorite.name}
|
||||
</span>
|
||||
</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,
|
||||
}
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { DragHandle, Tooltip } from "@plane/ui";
|
||||
// helper
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
sort_order: number | null;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteItemDragHandle: FC<Props> = observer((props) => {
|
||||
const { sort_order, isDragging } = props;
|
||||
// store hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
<div
|
||||
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": sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import { MoreHorizontal, Star } from "lucide-react";
|
||||
import { IFavorite } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
ref: React.MutableRefObject<HTMLDivElement | null>;
|
||||
isMenuActive: boolean;
|
||||
favorite: IFavorite;
|
||||
onChange: (value: boolean) => void;
|
||||
handleRemoveFromFavorites: (favorite: IFavorite) => void;
|
||||
};
|
||||
|
||||
export const FavoriteItemQuickAction: FC<Props> = (props) => {
|
||||
const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props;
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={ref}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
onClick={() => onChange(!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,
|
||||
}
|
||||
)}
|
||||
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 flex-shrink-0" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
title: string;
|
||||
icon: JSX.Element;
|
||||
isSidebarCollapsed: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteItemTitle: FC<Props> = (props) => {
|
||||
const { href, title, icon, isSidebarCollapsed } = props;
|
||||
|
||||
const linkClass = "flex items-center gap-1.5 truncate w-full";
|
||||
const collapsedClass =
|
||||
"group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto";
|
||||
|
||||
return (
|
||||
<Link href={href} className={isSidebarCollapsed ? collapsedClass : linkClass} draggable>
|
||||
<span className="flex items-center justify-center size-5">{icon}</span>
|
||||
{!isSidebarCollapsed && <span className="text-sm leading-5 font-medium flex-1 truncate">{title}</span>}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
elementRef: React.RefObject<HTMLDivElement>;
|
||||
isMenuActive?: boolean;
|
||||
sidebarCollapsed?: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteItemWrapper: FC<Props> = (props) => {
|
||||
const { children, elementRef, isMenuActive = false, sidebarCollapsed = false } = props;
|
||||
return (
|
||||
<>
|
||||
{sidebarCollapsed ? (
|
||||
<div ref={elementRef}>{children}</div>
|
||||
) : (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
"use client";
|
||||
// lucide
|
||||
import { Briefcase, FileText, Layers } from "lucide-react";
|
||||
// types
|
||||
import { IFavorite, TLogoProps } from "@plane/types";
|
||||
// ui
|
||||
import { ContrastIcon, DiceIcon, FavoriteFolderIcon } from "@plane/ui";
|
||||
import { Logo } from "@/components/common";
|
||||
|
||||
const iconClassName = `flex-shrink-0 size-4 stroke-[1.5] m-auto`;
|
||||
|
||||
export const FAVORITE_ITEM_ICON: Record<string, JSX.Element> = {
|
||||
page: <FileText className={iconClassName} />,
|
||||
project: <Briefcase className={iconClassName} />,
|
||||
view: <Layers className={iconClassName} />,
|
||||
module: <DiceIcon className={iconClassName} />,
|
||||
cycle: <ContrastIcon className={iconClassName} />,
|
||||
folder: <FavoriteFolderIcon className={iconClassName} />,
|
||||
};
|
||||
|
||||
export const getFavoriteItemIcon = (type: string, logo?: TLogoProps | undefined) => (
|
||||
<>
|
||||
<div className="hidden group-hover:flex items-center justify-center size-5">
|
||||
{FAVORITE_ITEM_ICON[type] || <FileText className={iconClassName} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center size-5 group-hover:hidden">
|
||||
{logo?.in_use ? (
|
||||
<Logo logo={logo} size={16} type={type === "project" ? "material" : "lucide"} />
|
||||
) : (
|
||||
FAVORITE_ITEM_ICON[type] || <FileText className={iconClassName} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const entityPaths: Record<string, string> = {
|
||||
project: "issues",
|
||||
cycle: "cycles",
|
||||
module: "modules",
|
||||
view: "views",
|
||||
page: "pages",
|
||||
};
|
||||
|
||||
export const generateFavoriteItemLink = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const entityPath = entityPaths[favorite.entity_type];
|
||||
return entityPath
|
||||
? `/${workspaceSlug}/projects/${favorite.project_id}/${entityPath}/${favorite.entity_identifier || ""}`
|
||||
: `/${workspaceSlug}`;
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./favorite-item-drag-handle";
|
||||
export * from "./favorite-item-quick-action";
|
||||
export * from "./favorite-item-wrapper";
|
||||
export * from "./favorite-item-title";
|
||||
export * from "./helper";
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./common";
|
||||
export * from "./root";
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
"use client";
|
||||
|
||||
import React, { FC, useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { IFavorite } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
FavoriteItemDragHandle,
|
||||
FavoriteItemQuickAction,
|
||||
FavoriteItemWrapper,
|
||||
FavoriteItemTitle,
|
||||
} from "@/components/workspace/sidebar/favorites";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useFavoriteItemDetails } from "@/hooks/use-favorite-item-details";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
favorite: IFavorite;
|
||||
favoriteMap: Record<string, IFavorite>;
|
||||
handleRemoveFromFavorites: (favorite: IFavorite) => void;
|
||||
handleRemoveFromFavoritesFolder: (favoriteId: string) => void;
|
||||
};
|
||||
|
||||
export const FavoriteRoot: FC<Props> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, favorite, favoriteMap, handleRemoveFromFavorites, handleRemoveFromFavoritesFolder } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
|
||||
//state
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
//ref
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleQuickAction = (value: boolean) => setIsMenuActive(value);
|
||||
|
||||
const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite);
|
||||
|
||||
// drag and drop
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
dragHandle: elementRef.current,
|
||||
canDrag: () => true,
|
||||
getInitialData: () => ({ id: favorite.id, type: "CHILD" }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDragEnter: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onDrop: ({ source }) => {
|
||||
setIsDragging(false);
|
||||
const sourceId = source?.data?.id as string | undefined;
|
||||
if (!sourceId || !favoriteMap[sourceId].parent) return;
|
||||
handleRemoveFromFavoritesFolder(sourceId);
|
||||
},
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, isDragging]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
return (
|
||||
<>
|
||||
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive} sidebarCollapsed={sidebarCollapsed}>
|
||||
{!sidebarCollapsed && <FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />}
|
||||
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} isSidebarCollapsed={!!sidebarCollapsed} />
|
||||
{!sidebarCollapsed && (
|
||||
<FavoriteItemQuickAction
|
||||
favorite={favorite}
|
||||
ref={actionSectionRef}
|
||||
isMenuActive={isMenuActive}
|
||||
onChange={handleQuickAction}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
/>
|
||||
)}
|
||||
</FavoriteItemWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -22,7 +22,7 @@ import useLocalStorage from "@/hooks/use-local-storage";
|
|||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { FavoriteFolder } from "./favorite-folder";
|
||||
import { FavoriteItem } from "./favorite-item";
|
||||
import { FavoriteRoot } from "./favorite-items";
|
||||
import { NewFavoriteFolder } from "./new-fav-folder";
|
||||
|
||||
export const SidebarFavoritesMenu = observer(() => {
|
||||
|
|
@ -196,7 +196,8 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
|
||||
/>
|
||||
) : (
|
||||
<FavoriteItem
|
||||
<FavoriteRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
favorite={fav}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
|
||||
|
|
|
|||
5
web/core/components/workspace/sidebar/favorites/index.ts
Normal file
5
web/core/components/workspace/sidebar/favorites/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./favorite-folder";
|
||||
export * from "./favorite-items";
|
||||
export * from "./favorites-menu";
|
||||
export * from "./favorites.helpers";
|
||||
export * from "./new-fav-folder";
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./dropdown";
|
||||
export * from "./favorites";
|
||||
export * from "./help-section";
|
||||
export * from "./projects-list-item";
|
||||
export * from "./projects-list";
|
||||
|
|
|
|||
58
web/core/hooks/use-favorite-item-details.tsx
Normal file
58
web/core/hooks/use-favorite-item-details.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { IFavorite } from "@plane/types";
|
||||
import {
|
||||
generateFavoriteItemLink,
|
||||
getFavoriteItemIcon,
|
||||
} from "@/components/workspace/sidebar/favorites/favorite-items/common";
|
||||
import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hooks/store";
|
||||
|
||||
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const favoriteItemId = favorite.entity_data.id;
|
||||
const favoriteItemLogoProps = favorite?.entity_data?.logo_props;
|
||||
const favoriteItemName = favorite?.entity_data.name || favorite?.name;
|
||||
const favoriteItemEntityType = favorite?.entity_type;
|
||||
|
||||
// store hooks
|
||||
const { getViewById } = useProjectView();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getModuleById } = useModule();
|
||||
|
||||
// derived values
|
||||
const pageDetail = usePage(favoriteItemId ?? "");
|
||||
const viewDetails = getViewById(favoriteItemId ?? "");
|
||||
const cycleDetail = getCycleById(favoriteItemId ?? "");
|
||||
const moduleDetail = getModuleById(favoriteItemId ?? "");
|
||||
|
||||
let itemIcon;
|
||||
let itemTitle;
|
||||
const itemLink = generateFavoriteItemLink(workspaceSlug.toString(), favorite);
|
||||
|
||||
switch (favoriteItemEntityType) {
|
||||
case "project":
|
||||
itemTitle = currentProjectDetails?.name || favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("project", currentProjectDetails?.logo_props || favoriteItemLogoProps);
|
||||
break;
|
||||
case "page":
|
||||
itemTitle = pageDetail.name || favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("page", pageDetail?.logo_props || favoriteItemLogoProps);
|
||||
break;
|
||||
case "view":
|
||||
itemTitle = viewDetails?.name || favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("view", viewDetails?.logo_props || favoriteItemLogoProps);
|
||||
break;
|
||||
case "cycle":
|
||||
itemTitle = cycleDetail?.name || favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("cycle");
|
||||
break;
|
||||
case "module":
|
||||
itemTitle = moduleDetail?.name || favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("module");
|
||||
break;
|
||||
default:
|
||||
itemTitle = favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon(favoriteItemEntityType);
|
||||
break;
|
||||
}
|
||||
|
||||
return { itemIcon, itemTitle, itemLink };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue