diff --git a/packages/types/src/favorite/favorite.d.ts b/packages/types/src/favorite/favorite.d.ts index 092a1209599..c8ec2509de9 100644 --- a/packages/types/src/favorite/favorite.d.ts +++ b/packages/types/src/favorite/favorite.d.ts @@ -15,6 +15,7 @@ export type IFavorite = { name: string; entity_type: string; entity_data: { + id?: string; name: string; logo_props?: TLogoProps | undefined; }; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 046bd4f81fc..63a683c5fc3 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -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) => { })} > {favorite.children.map((child) => ( - = { - page: , - project: , - view: , - module: , - cycle: , - issue: , - folder: , -}; - -export const FavoriteItem = observer( - ({ - favoriteMap, - favorite, - handleRemoveFromFavorites, - handleRemoveFromFavoritesFolder, - }: { - favorite: IFavorite; - favoriteMap: Record; - 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(null); - const dragHandleRef = useRef(null); - const actionSectionRef = useRef(null); - - const getIcon = () => ( - <> -
- {ICONS[favorite.entity_type] || } -
-
- {favorite.entity_data?.logo_props?.in_use ? ( - - ) : ( - ICONS[favorite.entity_type] || - )} -
- - ); - - 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 ? ( -
- - {getIcon()} - -
- ) : ( -
- - - - -
{getIcon()}
- - {favorite.entity_data ? favorite.entity_data.name : favorite.name} - - - 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, - } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - handleRemoveFromFavorites(favorite)}> - - - Remove from favorites - - - -
- )} - - ); - } -); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx new file mode 100644 index 00000000000..6c8e8666ea8 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx @@ -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 = observer((props) => { + const { sort_order, isDragging } = props; + // store hooks + const { isMobile } = usePlatformOS(); + + return ( + +
+ +
+
+ ); +}); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx new file mode 100644 index 00000000000..eadaedb3458 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx @@ -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; + isMenuActive: boolean; + favorite: IFavorite; + onChange: (value: boolean) => void; + handleRemoveFromFavorites: (favorite: IFavorite) => void; +}; + +export const FavoriteItemQuickAction: FC = (props) => { + const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props; + return ( + onChange(!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, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + handleRemoveFromFavorites(favorite)}> + + + Remove from favorites + + + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx new file mode 100644 index 00000000000..d8fc0d0b3bf --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx @@ -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) => { + 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 ( + + {icon} + {!isSidebarCollapsed && {title}} + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx new file mode 100644 index 00000000000..f1ffde408ca --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx @@ -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; + isMenuActive?: boolean; + sidebarCollapsed?: boolean; +}; + +export const FavoriteItemWrapper: FC = (props) => { + const { children, elementRef, isMenuActive = false, sidebarCollapsed = false } = props; + return ( + <> + {sidebarCollapsed ? ( +
{children}
+ ) : ( +
+ {children} +
+ )} + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx new file mode 100644 index 00000000000..8f7fb9859e2 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx @@ -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 = { + page: , + project: , + view: , + module: , + cycle: , + folder: , +}; + +export const getFavoriteItemIcon = (type: string, logo?: TLogoProps | undefined) => ( + <> +
+ {FAVORITE_ITEM_ICON[type] || } +
+
+ {logo?.in_use ? ( + + ) : ( + FAVORITE_ITEM_ICON[type] || + )} +
+ +); + +const entityPaths: Record = { + 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}`; +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts b/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts new file mode 100644 index 00000000000..5e03ce1c74f --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts @@ -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"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts b/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts new file mode 100644 index 00000000000..1037372f379 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts @@ -0,0 +1,2 @@ +export * from "./common"; +export * from "./root"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx new file mode 100644 index 00000000000..b001b7f69de --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx @@ -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; + handleRemoveFromFavorites: (favorite: IFavorite) => void; + handleRemoveFromFavoritesFolder: (favoriteId: string) => void; +}; + +export const FavoriteRoot: FC = 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(null); + const actionSectionRef = useRef(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 ( + <> + + {!sidebarCollapsed && } + + {!sidebarCollapsed && ( + + )} + + + ); +}); diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index d4a18fc402a..17380b079cc 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -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} /> ) : ( - { + 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 }; +};