From 0d762d74dceb7c8a8658870c95e23d00c09ddd74 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:21:24 +0530 Subject: [PATCH] fix: kanban board block's menu & drop delete. (#2987) * fix: kanban board block menu click * fix: menu active/disable * fix: drag n drop delete modal * fix: quick action button in all the layouts * chore: toast for drag & drop api --- .../calendar/base-calendar-root.tsx | 3 +- .../issue-layouts/calendar/calendar.tsx | 2 +- .../issue-layouts/calendar/day-tile.tsx | 2 +- .../issue-layouts/calendar/issue-blocks.tsx | 29 ++++++- .../issue-layouts/calendar/week-days.tsx | 2 +- .../issue-layouts/kanban/base-kanban-root.tsx | 75 +++++++++++++++++-- .../issues/issue-layouts/kanban/block.tsx | 41 ++++++++-- .../issue-layouts/kanban/blocks-list.tsx | 7 +- .../issues/issue-layouts/kanban/default.tsx | 14 +++- .../issue-layouts/kanban/roots/cycle-root.tsx | 4 +- .../kanban/roots/module-root.tsx | 4 +- .../kanban/roots/project-root.tsx | 4 +- .../kanban/roots/project-view-root.tsx | 4 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 14 +++- .../issue-layouts/list/list-view-types.d.ts | 1 + .../quick-action-dropdowns/all-issue.tsx | 4 +- .../quick-action-dropdowns/archived-issue.tsx | 4 +- .../quick-action-dropdowns/cycle-issue.tsx | 4 +- .../quick-action-dropdowns/module-issue.tsx | 4 +- .../quick-action-dropdowns/project-issue.tsx | 4 +- .../spreadsheet/base-spreadsheet-root.tsx | 3 +- .../columns/issue/issue-column.tsx | 36 +++++++-- .../issue/spreadsheet-issue-column.tsx | 2 +- .../spreadsheet/spreadsheet-view.tsx | 2 +- .../issues/base-issue-kanban-helper.store.ts | 11 +-- 25 files changed, 225 insertions(+), 55 deletions(-) diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 065476b2d..abdca8d55 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -81,8 +81,9 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { groupedIssueIds={groupedIssueIds} layout={displayFilters?.calendar?.layout} showWeekends={displayFilters?.calendar?.show_weekends ?? false} - quickActions={(issue) => ( + quickActions={(issue, customActionButton) => ( handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)} handleUpdate={ diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 7ac9df7bd..0240a8ebe 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -16,7 +16,7 @@ type Props = { groupedIssueIds: IGroupedIssues; layout: "month" | "week" | undefined; showWeekends: boolean; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickAddCallback?: ( workspaceSlug: string, projectId: string, diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index c0110e9f2..0bcf2d305 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -16,7 +16,7 @@ type Props = { date: ICalendarDate; issues: IIssueResponse | undefined; groupedIssueIds: IGroupedIssues; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( workspaceSlug: string, diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index d449e439a..8a0354f74 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,8 +1,12 @@ +import { useState, useRef } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Draggable } from "@hello-pangea/dnd"; +import { MoreHorizontal } from "lucide-react"; // components import { Tooltip } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssue } from "types"; import { IIssueResponse } from "store/issues/types"; @@ -10,7 +14,7 @@ import { IIssueResponse } from "store/issues/types"; type Props = { issues: IIssueResponse | undefined; issueIdList: string[] | null; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; }; export const CalendarIssueBlocks: React.FC = observer((props) => { @@ -18,6 +22,11 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { // router const router = useRouter(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + + const menuActionRef = useRef(null); + const handleIssuePeekOverview = (issue: IIssue) => { const { query } = router; @@ -27,6 +36,20 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { }); }; + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + return ( <> {issueIdList?.map((issueId, index) => { @@ -69,13 +92,13 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 9aee32e71..80db9b7b0 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -15,7 +15,7 @@ type Props = { issues: IIssueResponse | undefined; groupedIssueIds: IGroupedIssues; week: ICalendarWeek | undefined; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( workspaceSlug: string, diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index e5d279809..45e369f67 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,5 +1,5 @@ import { FC, useCallback, useState } from "react"; -import { DragDropContext, DropResult, Droppable } from "@hello-pangea/dnd"; +import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store @@ -24,13 +24,15 @@ import { } from "store/issues"; import { IQuickActionProps } from "../list/list-view-types"; import { IIssueKanBanViewStore } from "store/issue"; +// hooks +import useToast from "hooks/use-toast"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; import { EProjectStore } from "store/command-palette.store"; -import { IssuePeekOverview } from "components/issues"; +import { DeleteIssueModal, IssuePeekOverview } from "components/issues"; import { EUserWorkspaceRoles } from "constants/workspace"; export interface IBaseKanBanLayout { @@ -64,10 +66,16 @@ export interface IBaseKanBanLayout { groupBy: string | null, issues: any, issueWithIds: any - ) => void; + ) => Promise; addIssuesToView?: (issueIds: string[]) => Promise; } +type KanbanDragState = { + draggedIssueId?: string | null; + source?: DraggableLocation | null; + destination?: DraggableLocation | null; +}; + export const BaseKanBanRoot: React.FC = observer((props: IBaseKanBanLayout) => { const { issueStore, @@ -93,6 +101,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas user: userStore, } = useMobxStore(); + // hooks + const { setToastAlert } = useToast(); + const { currentProjectRole } = userStore; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; @@ -114,8 +125,15 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; + // states const [isDragStarted, setIsDragStarted] = useState(false); - const onDragStart = () => { + const [dragState, setDragState] = useState({}); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const onDragStart = (dragStart: DragStart) => { + setDragState({ + draggedIssueId: dragStart.draggableId.split("__")[0], + }); setIsDragStarted(true); }; @@ -134,7 +152,18 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas ) return; - if (handleDragDrop) handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds); + if (handleDragDrop) { + if (result.destination?.droppableId && result.destination?.droppableId.split("__")[0] === "issue-trash-box") { + setDragState({ + ...dragState, + source: result.source, + destination: result.destination, + }); + setDeleteIssueModal(true); + } else { + handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds); + } + } }; const handleIssues = useCallback( @@ -146,6 +175,29 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas [issueActions] ); + const handleDeleteIssue = async () => { + if (!handleDragDrop) return; + await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds) + .then(() => { + setToastAlert({ + title: "Success", + type: "success", + message: "Issue deleted successfully", + }); + }) + .catch(() => { + setToastAlert({ + title: "Error", + type: "error", + message: "Failed to delete issue", + }); + }) + .finally(() => { + setDeleteIssueModal(false); + setDragState({}); + }); + }; + const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { kanbanViewStore.handleKanBanToggle(toggle, value); }; @@ -156,6 +208,13 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas return ( <> + setDeleteIssueModal(false)} + onSubmit={handleDeleteIssue} + /> + {showLoader && issueStore?.loader === "init-loader" && (
@@ -194,8 +253,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas group_by={group_by} order_by={order_by} handleIssues={handleIssues} - quickActions={(sub_group_by, group_by, issue) => ( + quickActions={(sub_group_by, group_by, issue, customActionButton) => ( handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)} handleUpdate={ @@ -237,8 +297,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas group_by={group_by} order_by={order_by} handleIssues={handleIssues} - quickActions={(sub_group_by, group_by, issue) => ( + quickActions={(sub_group_by, group_by, issue, customActionButton) => ( handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)} handleUpdate={ diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 4b00361b0..f03101b12 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,14 +1,16 @@ -import { memo } from "react"; +import { memo, useRef, useState } from "react"; import { Draggable } from "@hello-pangea/dnd"; -import isEqual from "lodash/isEqual"; // components import { KanBanProperties } from "./properties"; // ui import { Tooltip } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssueDisplayProperties, IIssue } from "types"; import { EIssueActions } from "../types"; import { useRouter } from "next/router"; +import { MoreHorizontal } from "lucide-react"; interface IssueBlockProps { sub_group_id: string; @@ -18,7 +20,12 @@ interface IssueBlockProps { isDragDisabled: boolean; showEmptyGroup: boolean; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; isReadOnly: boolean; } @@ -39,6 +46,11 @@ export const KanBanIssueMemoBlock: React.FC = (props) => { // router const router = useRouter(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + + const menuActionRef = useRef(null); + const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE); }; @@ -56,6 +68,20 @@ export const KanBanIssueMemoBlock: React.FC = (props) => { if (columnId) draggableId = `${draggableId}__${columnId}`; if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`; + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + return ( <> @@ -79,11 +105,16 @@ export const KanBanIssueMemoBlock: React.FC = (props) => {
{issue.project_detail.identifier}-{issue.sequence_id}
-
+
diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 3ef4f4468..3e6728ca2 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -12,7 +12,12 @@ interface IssueBlocksListProps { isDragDisabled: boolean; showEmptyGroup: boolean; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; isReadOnly: boolean; } diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index e1e3f48b7..0a527e845 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -27,7 +27,12 @@ export interface IGroupByKanBan { isDragDisabled: boolean; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; showEmptyGroup: boolean; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; @@ -181,7 +186,12 @@ export interface IKanBan { order_by: string | null; sub_group_id?: string; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 6bc0db584..6f47b57d1 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -47,7 +47,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { }, }; - const handleDragDrop = ( + const handleDragDrop = async ( source: any, destination: any, subGroupBy: string | null, @@ -56,7 +56,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined ) => { if (kanBanHelperStore.handleDragDrop) - kanBanHelperStore.handleDragDrop( + return await kanBanHelperStore.handleDragDrop( source, destination, workspaceSlug, diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 20130269c..a683a710e 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -47,7 +47,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { }, }; - const handleDragDrop = ( + const handleDragDrop = async ( source: any, destination: any, subGroupBy: string | null, @@ -56,7 +56,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined ) => { if (kanBanHelperStore.handleDragDrop) - kanBanHelperStore.handleDragDrop( + return await kanBanHelperStore.handleDragDrop( source, destination, workspaceSlug, diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index aa17eedaa..5566ca028 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -38,7 +38,7 @@ export const KanBanLayout: React.FC = observer(() => { }, }; - const handleDragDrop = ( + const handleDragDrop = async ( source: any, destination: any, subGroupBy: string | null, @@ -47,7 +47,7 @@ export const KanBanLayout: React.FC = observer(() => { issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined ) => { if (kanBanHelperStore.handleDragDrop) - kanBanHelperStore.handleDragDrop( + return await kanBanHelperStore.handleDragDrop( source, destination, workspaceSlug, diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 1315d284a..b1dfc8bb7 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -38,7 +38,7 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => { }, }; - const handleDragDrop = ( + const handleDragDrop = async ( source: any, destination: any, subGroupBy: string | null, @@ -47,7 +47,7 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => { issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined ) => { if (kanBanHelperStore.handleDragDrop) - kanBanHelperStore.handleDragDrop( + return await kanBanHelperStore.handleDragDrop( source, destination, workspaceSlug, diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 1c6d23162..fa35f4722 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -82,7 +82,12 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { members: IUserLite[] | null; projects: IProject[] | null; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; @@ -200,7 +205,12 @@ export interface IKanBanSwimLanes { group_by: string | null; order_by: string | null; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; - quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + quickActions: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + customActionButton?: React.ReactElement + ) => React.ReactNode; displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index 8f3dd8cb0..efdd79cfc 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -3,4 +3,5 @@ export interface IQuickActionProps { handleDelete: () => Promise; handleUpdate?: (data: IIssue) => Promise; handleRemoveFromView?: () => Promise; + customActionButton?: React.ReactElement; } 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 f823a3e15..970bf2c16 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 @@ -14,7 +14,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EProjectStore } from "store/command-palette.store"; export const AllIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate } = props; + const { issue, handleDelete, handleUpdate, customActionButton } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -58,7 +58,7 @@ export const AllIssueQuickActions: React.FC = (props) => { }} currentStore={EProjectStore.PROJECT} /> - + { e.preventDefault(); 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 fd9235f3f..812d6102a 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 @@ -12,7 +12,7 @@ import { copyUrlToClipboard } from "helpers/string.helper"; import { IQuickActionProps } from "../list/list-view-types"; export const ArchivedIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete } = props; + const { issue, handleDelete, customActionButton } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -40,7 +40,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => handleClose={() => setDeleteIssueModal(false)} onSubmit={handleDelete} /> - + { e.preventDefault(); 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 6df87e023..ecf938ab3 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 @@ -14,7 +14,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EProjectStore } from "store/command-palette.store"; export const CycleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props; + const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -58,7 +58,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { }} currentStore={EProjectStore.CYCLE} /> - + { e.preventDefault(); 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 54f24ca3f..9145fb870 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 @@ -14,7 +14,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EProjectStore } from "store/command-palette.store"; export const ModuleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props; + const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -58,7 +58,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { }} currentStore={EProjectStore.MODULE} /> - + { e.preventDefault(); 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 f6a384461..b72ee04bb 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 @@ -14,7 +14,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EProjectStore } from "store/command-palette.store"; export const ProjectIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate } = props; + const { issue, handleDelete, handleUpdate, customActionButton } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -58,7 +58,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => }} currentStore={EProjectStore.PROJECT} /> - + { e.preventDefault(); diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index e0b4c8cb5..39c2d22a0 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -89,8 +89,9 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}} handleDisplayFilterUpdate={handleDisplayFiltersUpdate} issues={issues as IIssueUnGroupedStructure} - quickActions={(issue) => ( + quickActions={(issue, customActionButton) => ( handleIssues(issue, EIssueActions.DELETE)} handleUpdate={ diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx index d013a0c25..ba1d3eb75 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import React, { useRef, useState } from "react"; import { useRouter } from "next/router"; -import { ChevronRight } from "lucide-react"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; // components import { Tooltip } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssue, IIssueDisplayProperties } from "types"; @@ -11,7 +13,7 @@ type Props = { expanded: boolean; handleToggleExpand: (issueId: string) => void; properties: IIssueDisplayProperties; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; disableUserActions: boolean; nestingLevel: number; }; @@ -27,6 +29,10 @@ export const IssueColumn: React.FC = ({ }) => { // router const router = useRouter(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + + const menuActionRef = useRef(null); const handleIssuePeekOverview = (issue: IIssue) => { const { query } = router; @@ -39,6 +45,20 @@ export const IssueColumn: React.FC = ({ const paddingLeft = `${nestingLevel * 54}px`; + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + return ( <>
@@ -48,12 +68,18 @@ export const IssueColumn: React.FC = ({ style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}} >
- + {issue.project_detail?.identifier}-{issue.sequence_id} {!disableUserActions && ( -
{quickActions(issue)}
+ )}
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx index 91f5d9194..d368b4f1c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx @@ -12,7 +12,7 @@ type Props = { expandedIssues: string[]; setExpandedIssues: React.Dispatch>; properties: IIssueDisplayProperties; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue,customActionButton?: React.ReactElement) => React.ReactNode; disableUserActions: boolean; nestingLevel?: number; }; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index de69c2d4e..63fd390a1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -21,7 +21,7 @@ type Props = { members?: IUserLite[] | undefined; labels?: IIssueLabel[] | undefined; states?: IState[] | undefined; - quickActions: (issue: IIssue) => React.ReactNode; + quickActions: (issue: IIssue,customActionButton?: React.ReactElement) => React.ReactNode; handleIssues: (issue: IIssue, action: EIssueActions) => void; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( diff --git a/web/store/issues/base-issue-kanban-helper.store.ts b/web/store/issues/base-issue-kanban-helper.store.ts index e21c85e84..a966f8610 100644 --- a/web/store/issues/base-issue-kanban-helper.store.ts +++ b/web/store/issues/base-issue-kanban-helper.store.ts @@ -6,6 +6,7 @@ import { IViewIssuesStore } from "./project-issues/project-view/issue.store"; import { IProjectDraftIssuesStore } from "./project-issues/draft/issue.store"; import { IProfileIssuesStore } from "./profile/issue.store"; import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "./types"; +import { IIssue } from "types"; export interface IKanBanHelpers { // actions @@ -26,7 +27,7 @@ export interface IKanBanHelpers { issues: IIssueResponse | undefined, issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined, viewId?: string | null - ) => void; + ) => Promise; } export class KanBanHelpers implements IKanBanHelpers { @@ -119,8 +120,8 @@ export class KanBanHelpers implements IKanBanHelpers { const [removed] = sourceIssues.splice(source.index, 1); if (removed) { - if (viewId) store?.removeIssue(workspaceSlug, projectId, removed, viewId); - else store?.removeIssue(workspaceSlug, projectId, removed); + if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed, viewId); + else return await store?.removeIssue(workspaceSlug, projectId, removed); } } else { const sourceIssues = subGroupBy @@ -182,8 +183,8 @@ export class KanBanHelpers implements IKanBanHelpers { } if (updateIssue && updateIssue?.id) { - if (viewId) store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); - else store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); + else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); } } };