UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция workspace sidebar и views action-menu на общий канон

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 13:14:18 +03:00
parent 6d35fc7bee
commit 5cf2c2130a
13 changed files with 396 additions and 494 deletions

View File

@ -4,14 +4,15 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useState, useRef } from "react"; import { useMemo } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { LogOut, MoreHorizontal, Settings, Share2, ArchiveIcon } from "lucide-react"; import { LogOut, MoreHorizontal, Settings, Share2, ArchiveIcon } from "lucide-react";
// plane imports // plane imports
import { MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { LinkIcon } from "@plane/propel/icons"; import { LinkIcon } from "@plane/propel/icons";
import { CustomMenu } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown } from "@plane/ui";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -34,85 +35,81 @@ export function ProjectActionsMenu({
onLeaveProject, onLeaveProject,
onPublishModal, onPublishModal,
}: Props) { }: Props) {
// states
const [isMenuActive, setIsMenuActive] = useState(false);
// translation // translation
const { t } = useTranslation(); const { t } = useTranslation();
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// router // router
const navigate = useNavigate(); const navigate = useNavigate();
return ( const menuItems = useMemo<TContextMenuItem[]>(
<CustomMenu () => [
customButton={ ...(isAdmin
<span ? [
ref={actionSectionRef} {
className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1" key: "publish",
onClick={() => setIsMenuActive(!isMenuActive)} title: t("publish_project"),
icon: Share2,
action: onPublishModal,
},
]
: []),
{
key: "copy-link",
title: t("copy_link"),
icon: LinkIcon,
action: onCopyText,
},
...(isAuthorized
? [
{
key: "archives",
title: t("archives"),
icon: ArchiveIcon,
action: () => {
navigate(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
},
},
]
: []),
{
key: "settings",
title: t("settings"),
icon: Settings,
action: () => {
navigate(`/${workspaceSlug}/settings/projects/${project?.id}`);
},
},
...(!isAuthorized
? [
{
key: "leave-project",
action: onLeaveProject,
customContent: (
<div
className="flex items-center justify-start gap-2"
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
> >
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("leave_project")}</span>
</div>
),
},
]
: []),
],
[isAdmin, isAuthorized, navigate, onCopyText, onLeaveProject, onPublishModal, project?.id, t, workspaceSlug]
);
return (
<ActionDropdown
button={
<span className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
</span> </span>
} }
className="flex-shrink-0" className="flex-shrink-0"
customButtonClassName="grid place-items-center" buttonClassName="grid place-items-center"
placement="bottom-start" placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} items={menuItems}
useCaptureForOutsideClick />
closeOnSelect
onMenuClose={() => setIsMenuActive(false)}
>
{/* Publish project settings */}
{isAdmin && (
<CustomMenu.MenuItem onClick={onPublishModal}>
<div className="relative flex flex-shrink-0 items-center justify-start gap-2">
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm text-secondary transition-all duration-300 hover:bg-layer-1">
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
</div>
<div>{t("publish_project")}</div>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={onCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("copy_link")}</span>
</span>
</CustomMenu.MenuItem>
{isAuthorized && (
<CustomMenu.MenuItem
onClick={() => {
navigate(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
}}
>
<div className="flex cursor-pointer items-center justify-start gap-2">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("archives")}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() => {
navigate(`/${workspaceSlug}/settings/projects/${project?.id}`);
}}
>
<div className="flex cursor-pointer items-center justify-start gap-2">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("settings")}</span>
</div>
</CustomMenu.MenuItem>
{/* Leave project */}
{!isAuthorized && (
<CustomMenu.MenuItem
onClick={onLeaveProject}
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("leave_project")}</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
); );
} }

View File

@ -13,7 +13,7 @@ import { Disclosure } from "@headlessui/react";
import { ROLE, EUserPermissions, MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { ROLE, EUserPermissions, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { EUserProjectRoles, IUser, IWorkspaceMember, TProjectMembership } from "@plane/types"; import type { EUserProjectRoles, IUser, IWorkspaceMember, TProjectMembership } from "@plane/types";
import { CustomMenu, CustomSelect } from "@plane/ui"; import { ActionDropdown, CustomSelect } from "@plane/ui";
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
@ -69,23 +69,25 @@ export function NameColumn(props: NameProps) {
{first_name} {last_name} {first_name} {last_name}
</div> </div>
{(isAdmin || id === currentUser?.id) && ( {(isAdmin || id === currentUser?.id) && (
<CustomMenu <ActionDropdown
ellipsis
buttonClassName="p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
optionsClassName="p-1.5"
placement="bottom-end" placement="bottom-end"
> buttonClassName="p-0.5 opacity-0 transition-opacity group-hover:opacity-100"
<CustomMenu.MenuItem> items={[
{
key: "remove-member",
action: () => setRemoveMemberModal(rowData),
customContent: (
<div <div
className="flex cursor-pointer items-center gap-x-1 font-medium text-danger-primary" className="flex cursor-pointer items-center gap-x-1 font-medium text-danger-primary"
data-ph-element={MEMBER_TRACKER_ELEMENTS.PROJECT_MEMBER_TABLE_CONTEXT_MENU} data-ph-element={MEMBER_TRACKER_ELEMENTS.PROJECT_MEMBER_TABLE_CONTEXT_MENU}
onClick={() => setRemoveMemberModal(rowData)}
> >
<CircleMinus className="size-3.5 flex-shrink-0" /> <CircleMinus className="size-3.5 flex-shrink-0" />
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "} {rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
</div> </div>
</CustomMenu.MenuItem> ),
</CustomMenu> },
]}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -13,8 +13,8 @@ import { useTranslation } from "@plane/i18n";
import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons"; import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { CustomSelect, CustomMenu } from "@plane/ui"; import { ActionDropdown, CustomSelect } from "@plane/ui";
import { cn, copyTextToClipboard } from "@plane/utils"; import { copyTextToClipboard } from "@plane/utils";
// components // components
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove"; import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
// hooks // hooks
@ -185,41 +185,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
})} })}
</CustomSelect> </CustomSelect>
{isAdmin && ( {isAdmin && (
<CustomMenu ellipsis placement="bottom-end" closeOnSelect> <ActionDropdown placement="bottom-end" items={MENU_ITEMS} />
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={() => {
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-placeholder": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("whitespace-pre-line text-tertiary", {
"text-placeholder": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
)} )}
</div> </div>
</div> </div>

View File

@ -22,12 +22,11 @@ import { createRoot } from "react-dom/client";
import { Star, MoreHorizontal, GripVertical } from "lucide-react"; import { Star, MoreHorizontal, GripVertical } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// plane imports // plane imports
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { DraftIcon, FavoriteFolderIcon, ChevronRightIcon } from "@plane/propel/icons"; import { DraftIcon, FavoriteFolderIcon, ChevronRightIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import type { IFavorite, InstructionType } from "@plane/types"; import type { IFavorite, InstructionType } from "@plane/types";
import { CustomMenu, DropIndicator, DragHandle } from "@plane/ui"; import { ActionDropdown, DropIndicator, DragHandle } from "@plane/ui";
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// hooks // hooks
@ -58,7 +57,6 @@ export function FavoriteFolder(props: Props) {
const [folderToRename, setFolderToRename] = useState<string | boolean | null>(null); const [folderToRename, setFolderToRename] = useState<string | boolean | null>(null);
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined); const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
// refs // refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const elementRef = useRef<HTMLDivElement | null>(null); const elementRef = useRef<HTMLDivElement | null>(null);
// translation // translation
const { t } = useTranslation(); const { t } = useTranslation();
@ -135,9 +133,6 @@ export function FavoriteFolder(props: Props) {
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDragging, favorite.id, isLastChild, favorite.id]); }, [isDragging, favorite.id, isLastChild, favorite.id]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
return folderToRename ? ( return folderToRename ? (
<NewFavoriteFolder <NewFavoriteFolder
setCreateNewFolder={setFolderToRename} setCreateNewFolder={setFolderToRename}
@ -208,39 +203,44 @@ export function FavoriteFolder(props: Props) {
</Disclosure.Button> </Disclosure.Button>
</div> </div>
</Tooltip> </Tooltip>
<CustomMenu <ActionDropdown
customButton={ button={
<span <span className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
ref={actionSectionRef}
className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"
>
<MoreHorizontal className="size-3" /> <MoreHorizontal className="size-3" />
</span> </span>
} }
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
className={cn( className={cn(
"pointer-events-none flex-shrink-0 opacity-0 group-hover/project-item:pointer-events-auto group-hover/project-item:opacity-100", "pointer-events-none flex-shrink-0 opacity-0 group-hover/project-item:pointer-events-auto group-hover/project-item:opacity-100",
{ {
"pointer-events-auto opacity-100": isMenuActive, "pointer-events-auto opacity-100": isMenuActive,
} }
)} )}
customButtonClassName="grid place-items-center" buttonClassName="grid place-items-center"
placement="bottom-start" placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} onOpenChange={setIsMenuActive}
> items={[
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}> {
key: "remove-favorite-folder",
action: () => handleRemoveFromFavorites(favorite),
customContent: (
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<Star className="fill-yellow-500 stroke-yellow-500 h-3.5 w-3.5" /> <Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
<span>Remove from favorites</span> <span>Remove from favorites</span>
</span> </span>
</CustomMenu.MenuItem> ),
<CustomMenu.MenuItem onClick={() => setFolderToRename(favorite.id)}> },
{
key: "rename-favorite-folder",
action: () => setFolderToRename(favorite.id),
customContent: (
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<DraftIcon className="h-3.5 w-3.5 stroke-[1.5] text-tertiary" /> <DraftIcon className="h-3.5 w-3.5 stroke-[1.5] text-tertiary" />
<span>Rename Folder</span> <span>Rename Folder</span>
</div> </div>
</CustomMenu.MenuItem> ),
</CustomMenu> },
]}
/>
<Disclosure.Button <Disclosure.Button
as="button" as="button"
type="button" type="button"

View File

@ -8,49 +8,49 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { MoreHorizontal, Star } from "lucide-react"; import { MoreHorizontal, Star } from "lucide-react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n";
import type { IFavorite } from "@plane/types"; import type { IFavorite } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
type Props = { type Props = {
ref: React.MutableRefObject<HTMLDivElement | null>;
isMenuActive: boolean; isMenuActive: boolean;
favorite: IFavorite; favorite: IFavorite;
onChange: (value: boolean) => void; onOpenChange: (value: boolean) => void;
handleRemoveFromFavorites: (favorite: IFavorite) => void; handleRemoveFromFavorites: (favorite: IFavorite) => void;
}; };
export const FavoriteItemQuickAction = observer(function FavoriteItemQuickAction(props: Props) { export const FavoriteItemQuickAction = observer(function FavoriteItemQuickAction(props: Props) {
const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props; const { isMenuActive, onOpenChange, handleRemoveFromFavorites, favorite } = props;
// translation
const { t } = useTranslation();
return ( return (
<CustomMenu <ActionDropdown
customButton={ button={
<span ref={ref} className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"> <span className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
</span> </span>
} }
menuButtonOnClick={() => onChange(!isMenuActive)}
className={cn( className={cn(
"pointer-events-none flex-shrink-0 opacity-0 group-hover/project-item:pointer-events-auto group-hover/project-item:opacity-100", "pointer-events-none flex-shrink-0 opacity-0 group-hover/project-item:pointer-events-auto group-hover/project-item:opacity-100",
{ {
"pointer-events-auto opacity-100": isMenuActive, "pointer-events-auto opacity-100": isMenuActive,
} }
)} )}
customButtonClassName="grid place-items-center" buttonClassName="grid place-items-center"
placement="bottom-start" placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} onOpenChange={onOpenChange}
> items={[
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}> {
key: "remove-favorite",
action: () => handleRemoveFromFavorites(favorite),
customContent: (
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<Star className="fill-yellow-500 stroke-yellow-500 h-3.5 w-3.5 flex-shrink-0" /> <Star className="h-3.5 w-3.5 flex-shrink-0 fill-yellow-500 stroke-yellow-500" />
<span>Remove from favorites</span> <span>Remove from favorites</span>
</span> </span>
</CustomMenu.MenuItem> ),
</CustomMenu> },
]}
/>
); );
}); });

View File

@ -18,7 +18,6 @@ import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
// plane imports // plane imports
import { useOutsideClickDetector } from "@plane/hooks";
import type { IFavorite, InstructionType } from "@plane/types"; import type { IFavorite, InstructionType } from "@plane/types";
import { DropIndicator } from "@plane/ui"; import { DropIndicator } from "@plane/ui";
// hooks // hooks
@ -48,9 +47,6 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
//ref //ref
const elementRef = useRef<HTMLDivElement>(null); const elementRef = useRef<HTMLDivElement>(null);
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const handleQuickAction = (value: boolean) => setIsMenuActive(value);
// drag and drop // drag and drop
useEffect(() => { useEffect(() => {
@ -121,9 +117,6 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef?.current, isDragging, isLastChild, favorite.id]); }, [elementRef?.current, isDragging, isLastChild, favorite.id]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
return ( return (
<> <>
{isDragging && <DropIndicator isVisible={instruction === "reorder-above"} />} {isDragging && <DropIndicator isVisible={instruction === "reorder-above"} />}
@ -132,9 +125,8 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} /> <FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} />
<FavoriteItemQuickAction <FavoriteItemQuickAction
favorite={favorite} favorite={favorite}
ref={actionSectionRef}
isMenuActive={isMenuActive} isMenuActive={isMenuActive}
onChange={handleQuickAction} onOpenChange={setIsMenuActive}
handleRemoveFromFavorites={handleRemoveFromFavorites} handleRemoveFromFavorites={handleRemoveFromFavorites}
/> />
</FavoriteItemWrapper> </FavoriteItemWrapper>

View File

@ -10,10 +10,9 @@ import { HelpCircle, MessagesSquare, User } from "lucide-react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { PageIcon } from "@plane/propel/icons"; import { PageIcon } from "@plane/propel/icons";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
// components // components
import { ProductUpdatesModal } from "@/components/global"; import { ProductUpdatesModal } from "@/components/global";
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
// hooks // hooks
import { usePowerK } from "@/hooks/store/use-power-k"; import { usePowerK } from "@/hooks/store/use-power-k";
import { useChatSupport } from "@/hooks/use-chat-support"; import { useChatSupport } from "@/hooks/use-chat-support";
@ -33,75 +32,99 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
<> <>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} /> <ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<CustomMenu <ActionDropdown
customButton={ buttonAsChild
<AppSidebarItem button={
variant="button" <button type="button" className="group flex flex-col items-center justify-center gap-0.5 text-tertiary">
item={{ <div
icon: <HelpCircle className="size-5" />, className={`flex size-8 items-center justify-center gap-2 rounded-md ${
isActive: isNeedHelpOpen, isNeedHelpOpen
}} ? "bg-layer-transparent-selected text-secondary !text-icon-primary"
/> : "group-hover:text-icon-secondary group-hover:bg-layer-transparent-hover !text-icon-tertiary"
} }`}
// customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement="bottom-end"
maxHeight="lg"
closeOnSelect
> >
<CustomMenu.MenuItem onClick={() => window.open("https://go.plane.so/p-docs", "_blank")}> <HelpCircle className="size-5" />
<div className="flex items-center gap-x-2 rounded-sm text-11">
<PageIcon className="h-3.5 w-3.5 text-secondary" height={14} width={14} />
<span className="text-11">{t("documentation")}</span>
</div> </div>
</CustomMenu.MenuItem> </button>
{isChatSupportEnabled && ( }
<CustomMenu.MenuItem> placement="bottom-end"
menuClassName="w-64"
onOpenChange={setIsNeedHelpOpen}
items={[]}
menuContent={({ closeDropdown }) => (
<>
<button <button
type="button" type="button"
onClick={openChatSupport} onClick={() => {
className="flex w-full items-center gap-x-2 rounded-sm text-11 hover:bg-layer-1" window.open("https://go.plane.so/p-docs", "_blank");
closeDropdown();
}}
className="flex w-full items-center gap-x-2 rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
>
<PageIcon className="h-3.5 w-3.5 text-secondary" height={14} width={14} />
<span>{t("documentation")}</span>
</button>
{isChatSupportEnabled && (
<button
type="button"
onClick={() => {
openChatSupport();
closeDropdown();
}}
className="flex w-full items-center gap-x-2 rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
> >
<MessagesSquare className="h-3.5 w-3.5 text-secondary" /> <MessagesSquare className="h-3.5 w-3.5 text-secondary" />
<span className="text-11">{t("message_support")}</span> <span>{t("message_support")}</span>
</button> </button>
</CustomMenu.MenuItem>
)} )}
<CustomMenu.MenuItem onClick={() => window.open("mailto:sales@plane.so", "_blank")}> <button
<div className="flex items-center gap-x-2 rounded-sm text-11"> type="button"
onClick={() => {
window.open("mailto:sales@plane.so", "_blank");
closeDropdown();
}}
className="flex w-full items-center gap-x-2 rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
>
<User className="h-3.5 w-3.5 text-secondary" size={14} /> <User className="h-3.5 w-3.5 text-secondary" size={14} />
<span className="text-11">{t("contact_sales")}</span> <span>{t("contact_sales")}</span>
</div> </button>
</CustomMenu.MenuItem>
<div className="my-1 border-t border-subtle" /> <div className="my-1 border-t border-subtle" />
<CustomMenu.MenuItem>
<button <button
type="button" type="button"
onClick={() => toggleShortcutsListModal(true)} onClick={() => {
className="justify-sbg-layer-211 flex w-full items-center hover:bg-layer-1" toggleShortcutsListModal(true);
closeDropdown();
}}
className="flex w-full items-center rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
> >
<span className="text-11">{t("keyboard_shortcuts")}</span> <span>{t("keyboard_shortcuts")}</span>
</button> </button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button <button
type="button" type="button"
onClick={() => setProductUpdatesModalOpen(true)} onClick={() => {
className="justify-sbg-layer-211 flex w-full items-center hover:bg-layer-1" setProductUpdatesModalOpen(true);
closeDropdown();
}}
className="flex w-full items-center rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
> >
<span className="text-11">{t("whats_new")}</span> <span>{t("whats_new")}</span>
</button>
<button
type="button"
onClick={() => {
window.open("https://forum.plane.so", "_blank", "noopener,noreferrer");
closeDropdown();
}}
className="flex w-full items-center rounded-[0.9rem] px-2 py-2 text-left text-11 text-secondary transition-colors hover:bg-white/6"
>
<span>Forum</span>
</button> </button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => window.open("https://forum.plane.so", "_blank", "noopener,noreferrer")}>
<div className="flex items-center gap-x-2 rounded-sm text-11">
<span className="text-11">Forum</span>
</div>
</CustomMenu.MenuItem>
<div className="mt-1 border-t border-subtle px-1 pt-2 text-11 text-secondary"> <div className="mt-1 border-t border-subtle px-1 pt-2 text-11 text-secondary">
<PlaneVersionNumber /> <PlaneVersionNumber />
</div> </div>
</CustomMenu> </>
)}
/>
</> </>
); );
}); });

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
@ -24,7 +24,8 @@ import { Logo } from "@plane/propel/emoji-icon-picker";
import { LinkIcon, ArchiveIcon, ChevronRightIcon, WorkItemsIcon } from "@plane/propel/icons"; import { LinkIcon, ArchiveIcon, ChevronRightIcon, WorkItemsIcon } from "@plane/propel/icons";
import { IconButton } from "@plane/propel/icon-button"; import { IconButton } from "@plane/propel/icon-button";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import { CustomMenu, DropIndicator, DragHandle, ControlLink } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// components // components
import { DEFAULT_TAB_KEY, getTabUrl } from "@/components/navigation/tab-navigation-utils"; import { DEFAULT_TAB_KEY, getTabUrl } from "@/components/navigation/tab-navigation-utils";
@ -188,6 +189,21 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
: null, : null,
].filter((item): item is TProjectActionItem => Boolean(item)); ].filter((item): item is TProjectActionItem => Boolean(item));
const projectActionMenuItems = useMemo<TContextMenuItem[]>(
() =>
projectActionItems.map((item) => ({
key: item.key,
action: item.onClick,
customContent: (
<div className="flex items-center justify-start gap-2" data-ph-element={item.analytics}>
{item.icon}
<span>{item.label}</span>
</div>
),
})),
[projectActionItems]
);
useEffect(() => { useEffect(() => {
const element = projectRef.current; const element = projectRef.current;
const dragHandleElement = dragHandleRef.current; const dragHandleElement = dragHandleRef.current;
@ -405,8 +421,8 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
</ControlLink> </ControlLink>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{!renderInToolbarMenu && ( {!renderInToolbarMenu && (
<CustomMenu <ActionDropdown
customButton={ button={
<span className="grid place-items-center"> <span className="grid place-items-center">
<MoreHorizontal className="h-3.5 w-3.5 text-placeholder" /> <MoreHorizontal className="h-3.5 w-3.5 text-placeholder" />
</span> </span>
@ -417,34 +433,17 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
"pointer-events-auto opacity-100": isMenuActive, "pointer-events-auto opacity-100": isMenuActive,
} }
)} )}
customButtonClassName={cn( buttonClassName={cn(
"grid size-7 place-items-center rounded-full text-placeholder transition-colors hover:bg-layer-transparent-hover", "grid size-7 place-items-center rounded-full text-placeholder transition-colors hover:bg-layer-transparent-hover",
{ {
"bg-layer-transparent-hover": isMenuActive, "bg-layer-transparent-hover": isMenuActive,
} }
)} )}
placement="bottom-start" placement="bottom-start"
menuItemsClassName={renderInToolbarMenu ? "z-[220]" : ""}
portalElement={renderInToolbarMenu && typeof document !== "undefined" ? document.body : undefined} portalElement={renderInToolbarMenu && typeof document !== "undefined" ? document.body : undefined}
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} onOpenChange={setIsMenuActive}
useCaptureForOutsideClick items={projectActionMenuItems}
closeOnSelect />
menuButtonOnClick={() => setIsMenuActive((state) => !state)}
onMenuClose={() => setIsMenuActive(false)}
>
{projectActionItems.map((item) => (
<CustomMenu.MenuItem
key={item.key}
onClick={item.onClick}
data-ph-element={item.analytics}
>
<div className="flex items-center justify-start gap-2">
{item.icon}
<span>{item.label}</span>
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
)} )}
{isAccordionMode && ( {isAccordionMode && (
<IconButton <IconButton

View File

@ -5,7 +5,6 @@
*/ */
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Menu } from "@headlessui/react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { LogOut, Settings, Settings2 } from "lucide-react"; import { LogOut, Settings, Settings2 } from "lucide-react";
@ -13,11 +12,10 @@ import { LogOut, Settings, Settings2 } from "lucide-react";
import { GOD_MODE_URL } from "@plane/constants"; import { GOD_MODE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Avatar, CustomMenu } from "@plane/ui"; import { ActionDropdown, Avatar } from "@plane/ui";
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
// components // components
import { CoverImage } from "@/components/common/cover-image"; import { CoverImage } from "@/components/common/cover-image";
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
// hooks // hooks
import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useCommandPalette } from "@/hooks/store/use-command-palette";
@ -62,8 +60,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
else toggleAnySidebarDropdown(false); else toggleAnySidebarDropdown(false);
}, [isUserMenuOpen, toggleAnySidebarDropdown]); }, [isUserMenuOpen, toggleAnySidebarDropdown]);
const menuContent = ( const renderMenuContent = (closeDropdown: () => void) => (
<> <div className="flex flex-col gap-y-3">
<div className="relative h-29 w-full rounded-lg"> <div className="relative h-29 w-full rounded-lg">
<CoverImage <CoverImage
src={currentUser?.cover_image_url ?? undefined} src={currentUser?.cover_image_url ?? undefined}
@ -93,50 +91,68 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
</div> </div>
</div> </div>
<div> <div>
<CustomMenu.MenuItem <button
onClick={() => type="button"
onClick={() => {
toggleProfileSettingsModal({ toggleProfileSettingsModal({
activeTab: "general", activeTab: "general",
isOpen: true, isOpen: true,
}) });
} closeDropdown();
className="flex items-center gap-2" }}
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-secondary transition-colors hover:bg-white/6"
> >
<Settings className="size-3.5 shrink-0" /> <Settings className="size-3.5 shrink-0" />
{t("settings")} {t("settings")}
</CustomMenu.MenuItem> </button>
<CustomMenu.MenuItem <button
onClick={() => type="button"
onClick={() => {
toggleProfileSettingsModal({ toggleProfileSettingsModal({
activeTab: "preferences", activeTab: "preferences",
isOpen: true, isOpen: true,
}) });
} closeDropdown();
className="flex items-center gap-2" }}
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-secondary transition-colors hover:bg-white/6"
> >
<Settings2 className="size-3.5 shrink-0" /> <Settings2 className="size-3.5 shrink-0" />
{t("preferences")} {t("preferences")}
</CustomMenu.MenuItem> </button>
</div> </div>
<CustomMenu.MenuItem onClick={handleSignOut} className="flex items-center gap-2"> <button
type="button"
onClick={() => {
handleSignOut();
closeDropdown();
}}
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-secondary transition-colors hover:bg-white/6"
>
<LogOut className="size-3.5 shrink-0" /> <LogOut className="size-3.5 shrink-0" />
{t("sign_out")} {t("sign_out")}
</CustomMenu.MenuItem> </button>
{isUserInstanceAdmin && ( {isUserInstanceAdmin && (
<CustomMenu.MenuItem <button
onClick={() => router.push(GOD_MODE_URL)} type="button"
className="bg-accent-primary/20 text-accent-primary hover:bg-accent-primary/30 hover:text-accent-secondary" onClick={() => {
router.push(GOD_MODE_URL);
closeDropdown();
}}
className="rounded-[0.9rem] bg-accent-primary/20 px-2 py-2 text-left text-accent-primary transition-colors hover:bg-accent-primary/30 hover:text-accent-secondary"
> >
{t("enter_god_mode")} {t("enter_god_mode")}
</CustomMenu.MenuItem> </button>
)} )}
</> </div>
); );
if (isToolbarVariant) {
return ( return (
<Menu as="div" className="relative"> <ActionDropdown
<Menu.Button className="flex items-center"
buttonAsChild
button={
isToolbarVariant ? (
<button
type="button" type="button"
aria-label={t("profile")} aria-label={t("profile")}
className="flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]" className="flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
@ -147,60 +163,44 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
size={18} size={18}
shape="circle" shape="circle"
/> />
</Menu.Button> </button>
) : isSidebarUtilityVariant ? (
<Menu.Items className="absolute top-full left-0 z-[170] mt-2 origin-top-left"> <button
<div className="nodedc-glass-modal nodedc-glass-popup-surface flex w-72 flex-col gap-y-3 rounded-[1.25rem] border-0 p-3 shadow-none outline-none"> type="button"
{menuContent} aria-label={t("profile")}
</div> className="flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
</Menu.Items> >
</Menu>
);
}
return (
<CustomMenu
className="flex items-center"
customButtonClassName={
isSidebarUtilityVariant
? "flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
: ""
}
customButton={
isSidebarUtilityVariant ? (
<span className="pointer-events-none flex size-8 items-center justify-center">
<Avatar <Avatar
name={currentUser?.display_name} name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")} src={getFileURL(currentUser?.avatar_url ?? "")}
size={18} size={18}
shape="circle" shape="circle"
/> />
</span> </button>
) : ( ) : (
<AppSidebarItem <button type="button" className="group flex flex-col items-center justify-center gap-0.5 text-tertiary">
variant="button" <div
item={{ className={`flex size-8 items-center justify-center gap-2 rounded-md ${
icon: ( isUserMenuOpen
? "bg-layer-transparent-selected text-secondary !text-icon-primary"
: "group-hover:text-icon-secondary group-hover:bg-layer-transparent-hover !text-icon-tertiary"
}`}
>
<Avatar <Avatar
name={currentUser?.display_name} name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")} src={getFileURL(currentUser?.avatar_url ?? "")}
size={20} size={20}
shape="circle" shape="circle"
/> />
), </div>
isActive: isUserMenuOpen, </button>
}}
/>
) )
} }
menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)}
onMenuClose={() => setIsUserMenuOpen(false)}
placement="bottom-end" placement="bottom-end"
maxHeight="2xl" menuClassName="w-72 p-3"
optionsClassName="w-72 p-3 flex flex-col gap-y-3" onOpenChange={setIsUserMenuOpen}
closeOnSelect items={[]}
> menuContent={({ closeDropdown }) => renderMenuContent(closeDropdown)}
{menuContent} />
</CustomMenu>
); );
}); });

View File

@ -4,18 +4,17 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useState, useRef } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { MoreHorizontal, ArchiveIcon, Settings } from "lucide-react"; import { MoreHorizontal, ArchiveIcon, Settings } from "lucide-react";
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
// plane imports // plane imports
import { EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { ChevronRightIcon } from "@plane/propel/icons"; import { ChevronRightIcon } from "@plane/propel/icons";
import { EUserWorkspaceRoles } from "@plane/types"; import { EUserWorkspaceRoles } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// store hooks // store hooks
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
@ -31,16 +30,11 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
const { isWorkspaceMenuOpen, toggleWorkspaceMenu } = props; const { isWorkspaceMenuOpen, toggleWorkspaceMenu } = props;
// state // state
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// hooks // hooks
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const router = useRouter(); const router = useRouter();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
const { t } = useTranslation(); const { t } = useTranslation();
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
// TODO: fix types // TODO: fix types
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const isAdmin = allowPermissions([EUserWorkspaceRoles.ADMIN] as any, EUserPermissionsLevel.WORKSPACE); const isAdmin = allowPermissions([EUserWorkspaceRoles.ADMIN] as any, EUserPermissionsLevel.WORKSPACE);
@ -54,15 +48,9 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
> >
<span>{t("workspace")}</span> <span>{t("workspace")}</span>
</Disclosure.Button> </Disclosure.Button>
<CustomMenu <ActionDropdown
customButton={ button={
<span <span className="my-auto grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
ref={actionSectionRef}
className="my-auto grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"
onClick={() => {
setIsMenuActive(!isMenuActive);
}}
>
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
</span> </span>
} }
@ -72,25 +60,25 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
"pointer-events-auto opacity-100": isMenuActive, "pointer-events-auto opacity-100": isMenuActive,
} }
)} )}
customButtonClassName="grid place-items-center" buttonClassName="grid place-items-center"
placement="bottom-start" placement="bottom-start"
> onOpenChange={setIsMenuActive}
<CustomMenu.MenuItem onClick={() => router.push(`/${workspaceSlug}/projects/archives`)}> items={[
<div className="flex items-center justify-start gap-2"> {
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" /> key: "archives",
<span>{t("archives")}</span> title: t("archives"),
</div> icon: ArchiveIcon,
</CustomMenu.MenuItem> action: () => router.push(`/${workspaceSlug}/projects/archives`),
},
{isAdmin && ( {
<CustomMenu.MenuItem onClick={() => router.push(`/${workspaceSlug}/settings`)}> key: "settings",
<div className="flex items-center justify-start gap-2"> title: t("settings"),
<Settings className="h-3.5 w-3.5 stroke-[1.5]" /> icon: Settings,
<span>{t("settings")}</span> action: () => router.push(`/${workspaceSlug}/settings`),
</div> shouldRender: isAdmin,
</CustomMenu.MenuItem> },
)} ]}
</CustomMenu> />
<Disclosure.Button <Disclosure.Button
as="button" as="button"
className="group/workspace-button sticky top-0 z-10 flex items-center justify-between gap-1 rounded-sm px-0.5 py-1.5 text-11 font-semibold text-placeholder hover:bg-surface-2" className="group/workspace-button sticky top-0 z-10 flex items-center justify-between gap-1 rounded-sm px-0.5 py-1.5 text-11 font-semibold text-placeholder hover:bg-surface-2"

View File

@ -13,8 +13,8 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// ui // ui
import type { TStaticViewTypes } from "@plane/types"; import type { TStaticViewTypes } from "@plane/types";
import type { TContextMenuItem } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { CustomMenu } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils"; import { copyUrlToClipboard } from "@plane/utils";
// helpers // helpers
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -57,46 +57,12 @@ export const DefaultWorkspaceViewQuickActions = observer(function DefaultWorkspa
return ( return (
<> <>
<CustomMenu <ActionDropdown
ellipsis className="flex-shrink-0"
placement="bottom-end" placement="bottom-end"
closeOnSelect buttonClassName="flex size-[26px] items-center justify-center rounded-sm bg-layer-1/70"
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-layer-1/70 rounded-sm" items={MENU_ITEMS}
> />
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={() => {
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-placeholder": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{t(item.title || "")}</h5>
{item.description && (
<p
className={cn("whitespace-pre-line text-tertiary", {
"text-placeholder": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</> </>
); );
}); });

View File

@ -10,8 +10,8 @@ import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspaceView } from "@plane/types"; import type { IWorkspaceView } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils"; import { copyUrlToClipboard } from "@plane/utils";
// helpers // helpers
import { useViewMenuItems } from "@/components/common/quick-actions-helper"; import { useViewMenuItems } from "@/components/common/quick-actions-helper";
// hooks // hooks
@ -64,46 +64,12 @@ export const WorkspaceViewQuickActions = observer(function WorkspaceViewQuickAct
<> <>
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} /> <CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
<DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} /> <DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
<CustomMenu <ActionDropdown
ellipsis className="flex-shrink-0"
placement="bottom-end" placement="bottom-end"
closeOnSelect buttonClassName="flex size-[26px] items-center justify-center rounded-sm bg-layer-1/70"
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-layer-1/70 rounded-sm" items={MENU_ITEMS.items}
> />
{MENU_ITEMS.items.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={() => {
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-placeholder": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("whitespace-pre-line text-tertiary", {
"text-placeholder": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</> </>
); );
}); });

View File

@ -11,7 +11,8 @@ import { useParams } from "next/navigation";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { EditIcon, TrashIcon } from "@plane/propel/icons"; import { EditIcon, TrashIcon } from "@plane/propel/icons";
import { CustomMenu } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown } from "@plane/ui";
import { truncateText } from "@plane/utils"; import { truncateText } from "@plane/utils";
// hooks // hooks
import { useGlobalView } from "@/hooks/store/use-global-view"; import { useGlobalView } from "@/hooks/store/use-global-view";
@ -36,6 +37,25 @@ export const GlobalViewListItem = observer(function GlobalViewListItem(props: Pr
if (!view) return null; if (!view) return null;
const menuItems: TContextMenuItem[] = [
{
key: "edit",
title: t("common.actions.edit"),
icon: EditIcon,
action: () => {
setUpdateViewModal(true);
},
},
{
key: "delete",
title: t("common.actions.delete"),
icon: TrashIcon,
action: () => {
setDeleteViewModal(true);
},
},
];
return ( return (
<> <>
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} /> <CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
@ -52,28 +72,11 @@ export const GlobalViewListItem = observer(function GlobalViewListItem(props: Pr
</div> </div>
<div className="ml-2 flex flex-shrink-0"> <div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CustomMenu ellipsis> <ActionDropdown
<CustomMenu.MenuItem placement="bottom-end"
onClick={() => { buttonClassName="grid size-7 place-items-center rounded-sm text-secondary transition-colors hover:bg-layer-transparent-hover"
setUpdateViewModal(true); items={menuItems}
}} />
>
<span className="flex items-center justify-start gap-2">
<EditIcon width={14} height={14} strokeWidth={2} />
<span>{t("common.actions.edit")}</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setDeleteViewModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<TrashIcon width={14} height={14} strokeWidth={2} />
<span>{t("common.actions.delete")}</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div> </div>
</div> </div>
</div> </div>