diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 7a73a25f9..b7825fc57 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -161,6 +161,7 @@ export const AllIssueQuickActions: React.FC = observer((props portalElement={portalElement} placement={placements} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 0327755a9..62b808b3f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -123,6 +123,7 @@ export const ArchivedIssueQuickActions: React.FC = observer(( portalElement={portalElement} placement={placements} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 503d8258e..a35de2735 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -181,6 +181,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro customButton={customActionButton} portalElement={portalElement} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx index 18c259107..bbeda85ce 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx @@ -107,6 +107,7 @@ export const DraftIssueQuickActions: React.FC = observer((pro portalElement={portalElement} placement={placements} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 3cc3343b6..6061c0bee 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -178,6 +178,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr customButton={customActionButton} portalElement={portalElement} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 0fbe10da9..b74c9c57d 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -171,6 +171,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p customButton={customActionButton} portalElement={portalElement} menuItemsClassName="z-[14]" + maxHeight="lg" closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/components/workspace/views/default-view-quick-action.tsx b/web/components/workspace/views/default-view-quick-action.tsx new file mode 100644 index 000000000..5a58e1737 --- /dev/null +++ b/web/components/workspace/views/default-view-quick-action.tsx @@ -0,0 +1,127 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +import { ExternalLink, LinkIcon } from "lucide-react"; +// ui +import { TStaticViewTypes } from "@plane/types"; +import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { copyUrlToClipboard } from "@/helpers/string.helper"; + +type Props = { + parentRef: React.RefObject; + workspaceSlug: string; + globalViewId: string | undefined; + view: { + key: TStaticViewTypes; + label: string; + }; +}; + +export const DefaultWorkspaceViewQuickActions: React.FC = observer((props) => { + const { parentRef, globalViewId, view, workspaceSlug } = props; + + const viewLink = `${workspaceSlug}/workspace-views/${view.key}`; + const handleCopyText = () => + copyUrlToClipboard(viewLink).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link Copied!", + message: "View link copied to clipboard.", + }); + }); + const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); + + const MENU_ITEMS: TContextMenuItem[] = [ + { + key: "open-new-tab", + action: handleOpenInNewTab, + title: "Open in new tab", + icon: ExternalLink, + }, + { + key: "copy-link", + action: handleCopyText, + title: "Copy link", + icon: LinkIcon, + }, + ]; + + return ( + <> + + + + {view.key === globalViewId ? ( + + {view.label} + + ) : ( + + + {view.label} + + + )} + + } + placement="bottom-end" + menuItemsClassName="z-20" + closeOnSelect + > + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+
+ ); + })} +
+ + ); +}); diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index 35c01481b..2e381052a 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,11 +1,16 @@ import React, { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import Link from "next/link"; import { useRouter } from "next/router"; // icons import { Plus } from "lucide-react"; +// types +import { TStaticViewTypes } from "@plane/types"; // components -import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; +import { + CreateUpdateWorkspaceViewModal, + DefaultWorkspaceViewQuickActions, + WorkspaceViewQuickActions, +} from "@/components/workspace"; // constants import { GLOBAL_VIEW_OPENED } from "@/constants/event-tracker"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace"; @@ -14,6 +19,8 @@ import { useEventTracker, useGlobalView, useUser } from "@/hooks/store"; const ViewTab = observer((props: { viewId: string }) => { const { viewId } = props; + // refs + const parentRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, globalViewId } = router.query; @@ -22,30 +29,54 @@ const ViewTab = observer((props: { viewId: string }) => { const view = getViewDetailsById(viewId); - if (!view) return null; + if (!view || !workspaceSlug || !globalViewId) return null; return ( - - - {view.name} - - +
+ +
); }); +const DefaultViewTab = (props: { + tab: { + key: TStaticViewTypes; + label: string; + }; +}) => { + const { tab } = props; + // refs + const parentRef = useRef(null); + // router + const router = useRouter(); + const { workspaceSlug, globalViewId } = router.query; + + if (!workspaceSlug || !globalViewId) return null; + return ( +
+ +
+ ); +}; + export const GlobalViewsHeader: React.FC = observer(() => { // states const [createViewModal, setCreateViewModal] = useState(false); const containerRef = useRef(null); // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { globalViewId } = router.query; // store hooks const { currentWorkspaceViews } = useGlobalView(); const { @@ -82,23 +113,11 @@ export const GlobalViewsHeader: React.FC = observer(() => { ref={containerRef} className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm" > - {DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => ( - - - {tab.label} - - + {DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => ( + ))} - {currentWorkspaceViews?.map((viewId) => ( - - ))} + {currentWorkspaceViews?.map((viewId) => )} {isAuthorizedUser && ( diff --git a/web/components/workspace/views/index.ts b/web/components/workspace/views/index.ts index 7d0547f64..c41d75238 100644 --- a/web/components/workspace/views/index.ts +++ b/web/components/workspace/views/index.ts @@ -5,3 +5,5 @@ export * from "./header"; export * from "./modal"; export * from "./view-list-item"; export * from "./views-list"; +export * from "./quick-action"; +export * from "./default-view-quick-action"; diff --git a/web/components/workspace/views/quick-action.tsx b/web/components/workspace/views/quick-action.tsx new file mode 100644 index 000000000..3a67a95b9 --- /dev/null +++ b/web/components/workspace/views/quick-action.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react"; +// types +import { IWorkspaceView } from "@plane/types"; +// ui +import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; +// constants +import { EUserProjectRoles } from "@/constants/project"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { copyUrlToClipboard } from "@/helpers/string.helper"; +// hooks +import { useUser } from "@/hooks/store"; + +type Props = { + parentRef: React.RefObject; + workspaceSlug: string; + globalViewId: string; + viewId: string; + view: IWorkspaceView; +}; + +export const WorkspaceViewQuickActions: React.FC = observer((props) => { + const { parentRef, view, globalViewId, viewId, workspaceSlug } = props; + // states + const [updateViewModal, setUpdateViewModal] = useState(false); + const [deleteViewModal, setDeleteViewModal] = useState(false); + // store hooks + const { + membership: { currentWorkspaceRole }, + } = useUser(); + // auth + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserProjectRoles.MEMBER; + + const viewLink = `${workspaceSlug}/workspace-views/${view.id}`; + const handleCopyText = () => + copyUrlToClipboard(viewLink).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link Copied!", + message: "View link copied to clipboard.", + }); + }); + const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); + + const MENU_ITEMS: TContextMenuItem[] = [ + { + key: "edit", + action: () => setUpdateViewModal(true), + title: "Edit", + icon: Pencil, + shouldRender: isEditingAllowed, + }, + { + key: "open-new-tab", + action: handleOpenInNewTab, + title: "Open in new tab", + icon: ExternalLink, + }, + { + key: "copy-link", + action: handleCopyText, + title: "Copy link", + icon: LinkIcon, + }, + { + key: "delete", + action: () => setDeleteViewModal(true), + title: "Delete", + icon: Trash2, + shouldRender: isEditingAllowed, + }, + ]; + + return ( + <> + setUpdateViewModal(false)} /> + setDeleteViewModal(false)} /> + + + + + {viewId === globalViewId ? ( + + {view.name} + + ) : ( + + + {view.name} + + + )} + + } + placement="bottom-end" + menuItemsClassName="z-20" + closeOnSelect + > + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+
+ ); + })} +
+ + ); +});