UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция workspace sidebar и views action-menu на общий канон
This commit is contained in:
parent
6d35fc7bee
commit
5cf2c2130a
|
|
@ -4,14 +4,15 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { LogOut, MoreHorizontal, Settings, Share2, ArchiveIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { LinkIcon } from "@plane/propel/icons";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -34,85 +35,81 @@ export function ProjectActionsMenu({
|
|||
onLeaveProject,
|
||||
onPublishModal,
|
||||
}: Props) {
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
// router
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
const menuItems = useMemo<TContextMenuItem[]>(
|
||||
() => [
|
||||
...(isAdmin
|
||||
? [
|
||||
{
|
||||
key: "publish",
|
||||
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" />
|
||||
</span>
|
||||
}
|
||||
className="flex-shrink-0"
|
||||
customButtonClassName="grid place-items-center"
|
||||
buttonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
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>
|
||||
items={menuItems}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Disclosure } from "@headlessui/react";
|
|||
import { ROLE, EUserPermissions, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
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";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
|
|
@ -69,23 +69,25 @@ export function NameColumn(props: NameProps) {
|
|||
{first_name} {last_name}
|
||||
</div>
|
||||
{(isAdmin || id === currentUser?.id) && (
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
buttonClassName="p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
optionsClassName="p-1.5"
|
||||
<ActionDropdown
|
||||
placement="bottom-end"
|
||||
>
|
||||
<CustomMenu.MenuItem>
|
||||
buttonClassName="p-0.5 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
items={[
|
||||
{
|
||||
key: "remove-member",
|
||||
action: () => setRemoveMemberModal(rowData),
|
||||
customContent: (
|
||||
<div
|
||||
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}
|
||||
onClick={() => setRemoveMemberModal(rowData)}
|
||||
>
|
||||
<CircleMinus className="size-3.5 flex-shrink-0" />
|
||||
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { CustomSelect, CustomMenu } from "@plane/ui";
|
||||
import { cn, copyTextToClipboard } from "@plane/utils";
|
||||
import { ActionDropdown, CustomSelect } from "@plane/ui";
|
||||
import { copyTextToClipboard } from "@plane/utils";
|
||||
// components
|
||||
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
|
||||
// hooks
|
||||
|
|
@ -185,41 +185,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
|||
})}
|
||||
</CustomSelect>
|
||||
{isAdmin && (
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||
{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>
|
||||
<ActionDropdown placement="bottom-end" items={MENU_ITEMS} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,11 @@ import { createRoot } from "react-dom/client";
|
|||
import { Star, MoreHorizontal, GripVertical } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { DraftIcon, FavoriteFolderIcon, ChevronRightIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { IFavorite, InstructionType } from "@plane/types";
|
||||
import { CustomMenu, DropIndicator, DragHandle } from "@plane/ui";
|
||||
import { ActionDropdown, DropIndicator, DragHandle } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
|
|
@ -58,7 +57,6 @@ export function FavoriteFolder(props: Props) {
|
|||
const [folderToRename, setFolderToRename] = useState<string | boolean | null>(null);
|
||||
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -135,9 +133,6 @@ export function FavoriteFolder(props: Props) {
|
|||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDragging, favorite.id, isLastChild, favorite.id]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
return folderToRename ? (
|
||||
<NewFavoriteFolder
|
||||
setCreateNewFolder={setFolderToRename}
|
||||
|
|
@ -208,39 +203,44 @@ export function FavoriteFolder(props: Props) {
|
|||
</Disclosure.Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"
|
||||
>
|
||||
<ActionDropdown
|
||||
button={
|
||||
<span className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
|
||||
<MoreHorizontal className="size-3" />
|
||||
</span>
|
||||
}
|
||||
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
|
||||
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-auto opacity-100": isMenuActive,
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
buttonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
onOpenChange={setIsMenuActive}
|
||||
items={[
|
||||
{
|
||||
key: "remove-favorite-folder",
|
||||
action: () => handleRemoveFromFavorites(favorite),
|
||||
customContent: (
|
||||
<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>
|
||||
</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">
|
||||
<DraftIcon className="h-3.5 w-3.5 stroke-[1.5] text-tertiary" />
|
||||
<span>Rename Folder</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -8,49 +8,49 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { MoreHorizontal, Star } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { IFavorite } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
ref: React.MutableRefObject<HTMLDivElement | null>;
|
||||
isMenuActive: boolean;
|
||||
favorite: IFavorite;
|
||||
onChange: (value: boolean) => void;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
handleRemoveFromFavorites: (favorite: IFavorite) => void;
|
||||
};
|
||||
|
||||
export const FavoriteItemQuickAction = observer(function FavoriteItemQuickAction(props: Props) {
|
||||
const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
const { isMenuActive, onOpenChange, handleRemoveFromFavorites, favorite } = props;
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span ref={ref} className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
|
||||
<ActionDropdown
|
||||
button={
|
||||
<span className="grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
menuButtonOnClick={() => onChange(!isMenuActive)}
|
||||
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-auto opacity-100": isMenuActive,
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
buttonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
onOpenChange={onOpenChange}
|
||||
items={[
|
||||
{
|
||||
key: "remove-favorite",
|
||||
action: () => handleRemoveFromFavorites(favorite),
|
||||
customContent: (
|
||||
<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>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree
|
|||
import { observer } from "mobx-react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
// plane imports
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import type { IFavorite, InstructionType } from "@plane/types";
|
||||
import { DropIndicator } from "@plane/ui";
|
||||
// hooks
|
||||
|
|
@ -48,9 +47,6 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
|
|||
|
||||
//ref
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleQuickAction = (value: boolean) => setIsMenuActive(value);
|
||||
|
||||
// drag and drop
|
||||
useEffect(() => {
|
||||
|
|
@ -121,9 +117,6 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
|
|||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, isDragging, isLastChild, favorite.id]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
return (
|
||||
<>
|
||||
{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} />
|
||||
<FavoriteItemQuickAction
|
||||
favorite={favorite}
|
||||
ref={actionSectionRef}
|
||||
isMenuActive={isMenuActive}
|
||||
onChange={handleQuickAction}
|
||||
onOpenChange={setIsMenuActive}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
/>
|
||||
</FavoriteItemWrapper>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ import { HelpCircle, MessagesSquare, User } from "lucide-react";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageIcon } from "@plane/propel/icons";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
// components
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
// hooks
|
||||
import { usePowerK } from "@/hooks/store/use-power-k";
|
||||
import { useChatSupport } from "@/hooks/use-chat-support";
|
||||
|
|
@ -33,75 +32,99 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
|
|||
<>
|
||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<AppSidebarItem
|
||||
variant="button"
|
||||
item={{
|
||||
icon: <HelpCircle className="size-5" />,
|
||||
isActive: isNeedHelpOpen,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
// 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
|
||||
<ActionDropdown
|
||||
buttonAsChild
|
||||
button={
|
||||
<button type="button" className="group flex flex-col items-center justify-center gap-0.5 text-tertiary">
|
||||
<div
|
||||
className={`flex size-8 items-center justify-center gap-2 rounded-md ${
|
||||
isNeedHelpOpen
|
||||
? "bg-layer-transparent-selected text-secondary !text-icon-primary"
|
||||
: "group-hover:text-icon-secondary group-hover:bg-layer-transparent-hover !text-icon-tertiary"
|
||||
}`}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => window.open("https://go.plane.so/p-docs", "_blank")}>
|
||||
<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>
|
||||
<HelpCircle className="size-5" />
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isChatSupportEnabled && (
|
||||
<CustomMenu.MenuItem>
|
||||
</button>
|
||||
}
|
||||
placement="bottom-end"
|
||||
menuClassName="w-64"
|
||||
onOpenChange={setIsNeedHelpOpen}
|
||||
items={[]}
|
||||
menuContent={({ closeDropdown }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openChatSupport}
|
||||
className="flex w-full items-center gap-x-2 rounded-sm text-11 hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
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" />
|
||||
<span className="text-11">{t("message_support")}</span>
|
||||
<span>{t("message_support")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => window.open("mailto:sales@plane.so", "_blank")}>
|
||||
<div className="flex items-center gap-x-2 rounded-sm text-11">
|
||||
<button
|
||||
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} />
|
||||
<span className="text-11">{t("contact_sales")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<span>{t("contact_sales")}</span>
|
||||
</button>
|
||||
<div className="my-1 border-t border-subtle" />
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShortcutsListModal(true)}
|
||||
className="justify-sbg-layer-211 flex w-full items-center hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
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>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProductUpdatesModalOpen(true)}
|
||||
className="justify-sbg-layer-211 flex w-full items-center hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
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>
|
||||
</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">
|
||||
<PlaneVersionNumber />
|
||||
</div>
|
||||
</CustomMenu>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* 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 { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
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 { IconButton } from "@plane/propel/icon-button";
|
||||
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";
|
||||
// components
|
||||
import { DEFAULT_TAB_KEY, getTabUrl } from "@/components/navigation/tab-navigation-utils";
|
||||
|
|
@ -188,6 +189,21 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
|
|||
: null,
|
||||
].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(() => {
|
||||
const element = projectRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
|
|
@ -405,8 +421,8 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
|
|||
</ControlLink>
|
||||
<div className="flex items-center gap-1">
|
||||
{!renderInToolbarMenu && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<ActionDropdown
|
||||
button={
|
||||
<span className="grid place-items-center">
|
||||
<MoreHorizontal className="h-3.5 w-3.5 text-placeholder" />
|
||||
</span>
|
||||
|
|
@ -417,34 +433,17 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
|
|||
"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",
|
||||
{
|
||||
"bg-layer-transparent-hover": isMenuActive,
|
||||
}
|
||||
)}
|
||||
placement="bottom-start"
|
||||
menuItemsClassName={renderInToolbarMenu ? "z-[220]" : ""}
|
||||
portalElement={renderInToolbarMenu && typeof document !== "undefined" ? document.body : undefined}
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
useCaptureForOutsideClick
|
||||
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>
|
||||
onOpenChange={setIsMenuActive}
|
||||
items={projectActionMenuItems}
|
||||
/>
|
||||
)}
|
||||
{isAccordionMode && (
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 { useTranslation } from "@plane/i18n";
|
||||
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";
|
||||
// components
|
||||
import { CoverImage } from "@/components/common/cover-image";
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
|
|
@ -62,8 +60,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
|||
else toggleAnySidebarDropdown(false);
|
||||
}, [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">
|
||||
<CoverImage
|
||||
src={currentUser?.cover_image_url ?? undefined}
|
||||
|
|
@ -93,50 +91,68 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() =>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleProfileSettingsModal({
|
||||
activeTab: "general",
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
});
|
||||
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"
|
||||
>
|
||||
<Settings className="size-3.5 shrink-0" />
|
||||
{t("settings")}
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() =>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleProfileSettingsModal({
|
||||
activeTab: "preferences",
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
});
|
||||
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"
|
||||
>
|
||||
<Settings2 className="size-3.5 shrink-0" />
|
||||
{t("preferences")}
|
||||
</CustomMenu.MenuItem>
|
||||
</button>
|
||||
</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" />
|
||||
{t("sign_out")}
|
||||
</CustomMenu.MenuItem>
|
||||
</button>
|
||||
{isUserInstanceAdmin && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => router.push(GOD_MODE_URL)}
|
||||
className="bg-accent-primary/20 text-accent-primary hover:bg-accent-primary/30 hover:text-accent-secondary"
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</CustomMenu.MenuItem>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isToolbarVariant) {
|
||||
return (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
<ActionDropdown
|
||||
className="flex items-center"
|
||||
buttonAsChild
|
||||
button={
|
||||
isToolbarVariant ? (
|
||||
<button
|
||||
type="button"
|
||||
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]"
|
||||
|
|
@ -147,60 +163,44 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
|||
size={18}
|
||||
shape="circle"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
<Menu.Items className="absolute top-full left-0 z-[170] mt-2 origin-top-left">
|
||||
<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">
|
||||
{menuContent}
|
||||
</div>
|
||||
</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">
|
||||
</button>
|
||||
) : isSidebarUtilityVariant ? (
|
||||
<button
|
||||
type="button"
|
||||
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]"
|
||||
>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={18}
|
||||
shape="circle"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<AppSidebarItem
|
||||
variant="button"
|
||||
item={{
|
||||
icon: (
|
||||
<button type="button" className="group flex flex-col items-center justify-center gap-0.5 text-tertiary">
|
||||
<div
|
||||
className={`flex size-8 items-center justify-center gap-2 rounded-md ${
|
||||
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
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={20}
|
||||
shape="circle"
|
||||
/>
|
||||
),
|
||||
isActive: isUserMenuOpen,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)}
|
||||
onMenuClose={() => setIsUserMenuOpen(false)}
|
||||
placement="bottom-end"
|
||||
maxHeight="2xl"
|
||||
optionsClassName="w-72 p-3 flex flex-col gap-y-3"
|
||||
closeOnSelect
|
||||
>
|
||||
{menuContent}
|
||||
</CustomMenu>
|
||||
menuClassName="w-72 p-3"
|
||||
onOpenChange={setIsUserMenuOpen}
|
||||
items={[]}
|
||||
menuContent={({ closeDropdown }) => renderMenuContent(closeDropdown)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { MoreHorizontal, ArchiveIcon, Settings } from "lucide-react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ChevronRightIcon } from "@plane/propel/icons";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// store hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
|
|
@ -31,16 +30,11 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
|
|||
const { isWorkspaceMenuOpen, toggleWorkspaceMenu } = props;
|
||||
// state
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { workspaceSlug } = useParams();
|
||||
const router = useRouter();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
// TODO: fix types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isAdmin = allowPermissions([EUserWorkspaceRoles.ADMIN] as any, EUserPermissionsLevel.WORKSPACE);
|
||||
|
|
@ -54,15 +48,9 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
|
|||
>
|
||||
<span>{t("workspace")}</span>
|
||||
</Disclosure.Button>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="my-auto grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
setIsMenuActive(!isMenuActive);
|
||||
}}
|
||||
>
|
||||
<ActionDropdown
|
||||
button={
|
||||
<span className="my-auto grid place-items-center rounded-sm p-0.5 text-placeholder hover:bg-layer-1">
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
|
|
@ -72,25 +60,25 @@ export const SidebarWorkspaceMenuHeader = observer(function SidebarWorkspaceMenu
|
|||
"pointer-events-auto opacity-100": isMenuActive,
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
buttonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => router.push(`/${workspaceSlug}/projects/archives`)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
|
||||
{isAdmin && (
|
||||
<CustomMenu.MenuItem onClick={() => router.push(`/${workspaceSlug}/settings`)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
onOpenChange={setIsMenuActive}
|
||||
items={[
|
||||
{
|
||||
key: "archives",
|
||||
title: t("archives"),
|
||||
icon: ArchiveIcon,
|
||||
action: () => router.push(`/${workspaceSlug}/projects/archives`),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
title: t("settings"),
|
||||
icon: Settings,
|
||||
action: () => router.push(`/${workspaceSlug}/settings`),
|
||||
shouldRender: isAdmin,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Disclosure.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"
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
|||
// ui
|
||||
import type { TStaticViewTypes } from "@plane/types";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { copyUrlToClipboard, cn } from "@plane/utils";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// helpers
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -57,46 +57,12 @@ export const DefaultWorkspaceViewQuickActions = observer(function DefaultWorkspa
|
|||
|
||||
return (
|
||||
<>
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
<ActionDropdown
|
||||
className="flex-shrink-0"
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-layer-1/70 rounded-sm"
|
||||
>
|
||||
{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>
|
||||
buttonClassName="flex size-[26px] items-center justify-center rounded-sm bg-layer-1/70"
|
||||
items={MENU_ITEMS}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { observer } from "mobx-react";
|
|||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IWorkspaceView } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { copyUrlToClipboard, cn } from "@plane/utils";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// helpers
|
||||
import { useViewMenuItems } from "@/components/common/quick-actions-helper";
|
||||
// hooks
|
||||
|
|
@ -64,46 +64,12 @@ export const WorkspaceViewQuickActions = observer(function WorkspaceViewQuickAct
|
|||
<>
|
||||
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
||||
<DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
<ActionDropdown
|
||||
className="flex-shrink-0"
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-layer-1/70 rounded-sm"
|
||||
>
|
||||
{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>
|
||||
buttonClassName="flex size-[26px] items-center justify-center rounded-sm bg-layer-1/70"
|
||||
items={MENU_ITEMS.items}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { useParams } from "next/navigation";
|
|||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
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";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
|
|
@ -36,6 +37,25 @@ export const GlobalViewListItem = observer(function GlobalViewListItem(props: Pr
|
|||
|
||||
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 (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
||||
|
|
@ -52,28 +72,11 @@ export const GlobalViewListItem = observer(function GlobalViewListItem(props: Pr
|
|||
</div>
|
||||
<div className="ml-2 flex flex-shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setUpdateViewModal(true);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<ActionDropdown
|
||||
placement="bottom-end"
|
||||
buttonClassName="grid size-7 place-items-center rounded-sm text-secondary transition-colors hover:bg-layer-transparent-hover"
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue