-
Notifications
You must be signed in to change notification settings - Fork 3.6k
refactor: page actions menu #6076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d238bac
9ceb91c
bb00042
835440f
1314d3d
8416b48
436e4ac
c060024
84acc60
f0a41bd
9bcbf24
8806a67
3b6892d
383fb56
d52b747
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,7 +54,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { | |
| if (referenceElement) referenceElement.focus(); | ||
| }; | ||
| const closeDropdown = () => { | ||
| isOpen && onMenuClose && onMenuClose(); | ||
| if (isOpen) onMenuClose?.(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be cleaner if its written as |
||
| setIsOpen(false); | ||
| }; | ||
|
|
||
|
|
@@ -216,7 +216,7 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => { | |
| )} | ||
| onClick={(e) => { | ||
| close(); | ||
| onClick && onClick(e); | ||
| onClick?.(e); | ||
| }} | ||
| disabled={disabled} | ||
| > | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export * from "./editor"; | ||
| export * from "./modals"; | ||
| export * from "./extra-actions"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./move-page-modal"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // store types | ||
| import { IPage } from "@/store/pages/page"; | ||
|
|
||
| export type TMovePageModalProps = { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| page: IPage; | ||
| }; | ||
|
|
||
| export const MovePageModal: React.FC<TMovePageModalProps> = () => null; | ||
aaryan610 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,195 @@ | ||||||||||
| "use client"; | ||||||||||
|
|
||||||||||
| import { useMemo, useState } from "react"; | ||||||||||
| import { observer } from "mobx-react"; | ||||||||||
| import { | ||||||||||
| ArchiveRestoreIcon, | ||||||||||
| Copy, | ||||||||||
| ExternalLink, | ||||||||||
| FileOutput, | ||||||||||
| Globe2, | ||||||||||
| Link, | ||||||||||
| Lock, | ||||||||||
| LockKeyhole, | ||||||||||
| LockKeyholeOpen, | ||||||||||
| Trash2, | ||||||||||
| } from "lucide-react"; | ||||||||||
| // plane editor | ||||||||||
| import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; | ||||||||||
| // plane ui | ||||||||||
| import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; | ||||||||||
| // components | ||||||||||
| import { DeletePageModal } from "@/components/pages"; | ||||||||||
| // constants | ||||||||||
| import { EPageAccess } from "@/constants/page"; | ||||||||||
| // helpers | ||||||||||
| import { cn } from "@/helpers/common.helper"; | ||||||||||
| // hooks | ||||||||||
| import { usePageOperations } from "@/hooks/use-page-operations"; | ||||||||||
| // plane web components | ||||||||||
| import { MovePageModal } from "@/plane-web/components/pages"; | ||||||||||
| // store types | ||||||||||
| import { IPage } from "@/store/pages/page"; | ||||||||||
|
|
||||||||||
| export type TPageActions = | ||||||||||
| | "full-screen" | ||||||||||
| | "copy-markdown" | ||||||||||
| | "toggle-lock" | ||||||||||
| | "toggle-access" | ||||||||||
| | "open-in-new-tab" | ||||||||||
| | "copy-link" | ||||||||||
| | "make-a-copy" | ||||||||||
| | "archive-restore" | ||||||||||
| | "delete" | ||||||||||
| | "version-history" | ||||||||||
| | "export" | ||||||||||
| | "move"; | ||||||||||
|
|
||||||||||
| type Props = { | ||||||||||
| editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; | ||||||||||
| extraOptions?: (TContextMenuItem & { key: TPageActions })[]; | ||||||||||
| optionsOrder: TPageActions[]; | ||||||||||
| page: IPage; | ||||||||||
| parentRef?: React.RefObject<HTMLElement>; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export const PageActions: React.FC<Props> = observer((props) => { | ||||||||||
| const { editorRef, extraOptions, optionsOrder, page, parentRef } = props; | ||||||||||
| // states | ||||||||||
| const [deletePageModal, setDeletePageModal] = useState(false); | ||||||||||
| const [movePageModal, setMovePageModal] = useState(false); | ||||||||||
| // page operations | ||||||||||
| const { pageOperations } = usePageOperations({ | ||||||||||
| editorRef, | ||||||||||
| page, | ||||||||||
| }); | ||||||||||
| // derived values | ||||||||||
| const { | ||||||||||
| access, | ||||||||||
| archived_at, | ||||||||||
| is_locked, | ||||||||||
| canCurrentUserArchivePage, | ||||||||||
| canCurrentUserChangeAccess, | ||||||||||
| canCurrentUserDeletePage, | ||||||||||
| canCurrentUserDuplicatePage, | ||||||||||
| canCurrentUserLockPage, | ||||||||||
| canCurrentUserMovePage, | ||||||||||
| } = page; | ||||||||||
| // menu items | ||||||||||
| const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = useMemo( | ||||||||||
| () => [ | ||||||||||
| { | ||||||||||
| key: "toggle-lock", | ||||||||||
| action: pageOperations.toggleLock, | ||||||||||
| title: is_locked ? "Unlock" : "Lock", | ||||||||||
| icon: is_locked ? LockKeyholeOpen : LockKeyhole, | ||||||||||
| shouldRender: canCurrentUserLockPage, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "toggle-access", | ||||||||||
| action: pageOperations.toggleAccess, | ||||||||||
| title: access === EPageAccess.PUBLIC ? "Make private" : "Make public", | ||||||||||
| icon: access === EPageAccess.PUBLIC ? Lock : Globe2, | ||||||||||
| shouldRender: canCurrentUserChangeAccess && !archived_at, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "open-in-new-tab", | ||||||||||
| action: pageOperations.openInNewTab, | ||||||||||
| title: "Open in new tab", | ||||||||||
| icon: ExternalLink, | ||||||||||
| shouldRender: true, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "copy-link", | ||||||||||
| action: pageOperations.copyLink, | ||||||||||
| title: "Copy link", | ||||||||||
| icon: Link, | ||||||||||
| shouldRender: true, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "make-a-copy", | ||||||||||
| action: pageOperations.duplicate, | ||||||||||
| title: "Make a copy", | ||||||||||
| icon: Copy, | ||||||||||
| shouldRender: canCurrentUserDuplicatePage, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "archive-restore", | ||||||||||
| action: pageOperations.toggleArchive, | ||||||||||
| title: !!archived_at ? "Restore" : "Archive", | ||||||||||
| icon: !!archived_at ? ArchiveRestoreIcon : ArchiveIcon, | ||||||||||
|
Comment on lines
+119
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Simplify boolean conditions Remove redundant double negation operators for better readability. - title: !!archived_at ? "Restore" : "Archive",
- icon: !!archived_at ? ArchiveRestoreIcon : ArchiveIcon,
+ title: archived_at ? "Restore" : "Archive",
+ icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome[error] 113-113: Avoid redundant double-negation. It is not necessary to use double-negation when a value will already be coerced to a boolean. (lint/complexity/noExtraBooleanCast) [error] 114-114: Avoid redundant double-negation. It is not necessary to use double-negation when a value will already be coerced to a boolean. (lint/complexity/noExtraBooleanCast) |
||||||||||
| shouldRender: canCurrentUserArchivePage, | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| key: "delete", | ||||||||||
| action: () => setDeletePageModal(true), | ||||||||||
| title: "Delete", | ||||||||||
| icon: Trash2, | ||||||||||
| shouldRender: canCurrentUserDeletePage && !!archived_at, | ||||||||||
| }, | ||||||||||
|
|
||||||||||
| { | ||||||||||
| key: "move", | ||||||||||
| action: () => setMovePageModal(true), | ||||||||||
| title: "Move", | ||||||||||
| icon: FileOutput, | ||||||||||
| shouldRender: canCurrentUserMovePage, | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| [ | ||||||||||
| access, | ||||||||||
| archived_at, | ||||||||||
| is_locked, | ||||||||||
| canCurrentUserArchivePage, | ||||||||||
| canCurrentUserChangeAccess, | ||||||||||
| canCurrentUserDeletePage, | ||||||||||
| canCurrentUserDuplicatePage, | ||||||||||
| canCurrentUserLockPage, | ||||||||||
| canCurrentUserMovePage, | ||||||||||
| pageOperations, | ||||||||||
| ] | ||||||||||
| ); | ||||||||||
| if (extraOptions) { | ||||||||||
| MENU_ITEMS.push(...extraOptions); | ||||||||||
| } | ||||||||||
| // arrange options | ||||||||||
| const arrangedOptions = useMemo( | ||||||||||
| () => | ||||||||||
| optionsOrder | ||||||||||
| .map((key) => MENU_ITEMS.find((item) => item.key === key)) | ||||||||||
| .filter((item) => !!item) as (TContextMenuItem & { key: TPageActions })[], | ||||||||||
| [optionsOrder, MENU_ITEMS] | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <> | ||||||||||
| <MovePageModal isOpen={movePageModal} onClose={() => setMovePageModal(false)} page={page} /> | ||||||||||
| <DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={page.id ?? ""} /> | ||||||||||
| {parentRef && <ContextMenu parentRef={parentRef} items={arrangedOptions} />} | ||||||||||
| <CustomMenu placement="bottom-end" optionsClassName="max-h-[90vh]" ellipsis closeOnSelect> | ||||||||||
| {arrangedOptions.map((item) => { | ||||||||||
| if (item.shouldRender === false) return null; | ||||||||||
| return ( | ||||||||||
| <CustomMenu.MenuItem | ||||||||||
| key={item.key} | ||||||||||
| onClick={(e) => { | ||||||||||
| e.preventDefault(); | ||||||||||
| e.stopPropagation(); | ||||||||||
| item.action?.(); | ||||||||||
| }} | ||||||||||
| className={cn("flex items-center gap-2", item.className)} | ||||||||||
| disabled={item.disabled} | ||||||||||
| > | ||||||||||
| {item.customContent ?? ( | ||||||||||
| <> | ||||||||||
| {item.icon && <item.icon className="size-3" />} | ||||||||||
| {item.title} | ||||||||||
| </> | ||||||||||
| )} | ||||||||||
| </CustomMenu.MenuItem> | ||||||||||
| ); | ||||||||||
| })} | ||||||||||
| </CustomMenu> | ||||||||||
| </> | ||||||||||
| ); | ||||||||||
| }); | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| export * from "./actions"; | ||
| export * from "./edit-information-popover"; | ||
| export * from "./quick-actions"; |
Uh oh!
There was an error while loading. Please reload this page.