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 ad9903747..946ae0ed4 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -1,6 +1,5 @@ import { FC } from "react"; -import { DragDropContext, DropResult } from "@hello-pangea/dnd"; -import { observer } from "mobx-react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { TGroupedIssues } from "@plane/types"; // components @@ -51,67 +50,59 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; - const onDragEnd = async (result: DropResult) => { - if (!result) return; + const handleDragAndDrop = async ( + issueId: string | undefined, + sourceDate: string | undefined, + destinationDate: string | undefined + ) => { + if (!issueId || !destinationDate || !sourceDate) return; - // return if not dropped on the correct place - if (!result.destination) return; - - // return if dropped on the same date - if (result.destination.droppableId === result.source.droppableId) return; - - if (handleDragDrop) { - await handleDragDrop( - result.source, - result.destination, - workspaceSlug?.toString(), - projectId?.toString(), - issueMap, - groupedIssueIds, - updateIssue - ).catch((err) => { - setToast({ - title: "Error!", - type: TOAST_TYPE.ERROR, - message: err?.detail ?? "Failed to perform this action", - }); + await handleDragDrop( + issueId, + sourceDate, + destinationDate, + workspaceSlug?.toString(), + projectId?.toString(), + updateIssue + ).catch((err) => { + setToast({ + title: "Error!", + type: TOAST_TYPE.ERROR, + message: err?.detail ?? "Failed to perform this action", }); - } + }); }; return ( <>
- - ( - removeIssue(issue.project_id, issue.id)} - handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} - handleRemoveFromView={async () => - removeIssueFromView && removeIssueFromView(issue.project_id, issue.id) - } - handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} - handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} - readOnly={!isEditingAllowed || isCompletedCycle} - placements={placement} - /> - )} - addIssuesToView={addIssuesToView} - quickAddCallback={issues.quickAddIssue} - viewId={viewId} - readOnly={!isEditingAllowed || isCompletedCycle} - updateFilters={updateFilters} - /> - + ( + removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} + readOnly={!isEditingAllowed || isCompletedCycle} + placements={placement} + /> + )} + addIssuesToView={addIssuesToView} + quickAddCallback={issues.quickAddIssue} + viewId={viewId} + readOnly={!isEditingAllowed || isCompletedCycle} + updateFilters={updateFilters} + handleDragAndDrop={handleDragAndDrop} + />
); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index c4110bc13..a120c78d9 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,4 +1,6 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { observer } from "mobx-react-lite"; // types import type { @@ -40,6 +42,11 @@ type Props = { layout: "month" | "week" | undefined; showWeekends: boolean; quickActions: TRenderQuickActions; + handleDragAndDrop: ( + issueId: string | undefined, + sourceDate: string | undefined, + destinationDate: string | undefined + ) => Promise; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -63,6 +70,7 @@ export const CalendarChart: React.FC = observer((props) => { groupedIssueIds, layout, showWeekends, + handleDragAndDrop, quickActions, quickAddCallback, addIssuesToView, @@ -72,6 +80,8 @@ export const CalendarChart: React.FC = observer((props) => { } = props; // states const [selectedDate, setSelectedDate] = useState(new Date()); + //refs + const scrollableContainerRef = useRef(null); // store hooks const { issues: { viewFlags }, @@ -91,6 +101,19 @@ export const CalendarChart: React.FC = observer((props) => { const formattedDatePayload = renderFormattedPayloadDate(selectedDate) ?? undefined; + // Enable Auto Scroll for calendar + useEffect(() => { + const element = scrollableContainerRef.current; + + if (!element) return; + + return combine( + autoScrollForElements({ + element, + }) + ); + }, [scrollableContainerRef?.current]); + if (!calendarPayload || !formattedDatePayload) return (
@@ -112,6 +135,7 @@ export const CalendarChart: React.FC = observer((props) => { className={cn("flex w-full flex-col overflow-y-auto md:h-full", { "vertical-scrollbar scrollbar-lg": windowWidth > 768, })} + ref={scrollableContainerRef} >
@@ -123,6 +147,7 @@ export const CalendarChart: React.FC = observer((props) => { selectedDate={selectedDate} setSelectedDate={setSelectedDate} issuesFilterStore={issuesFilterStore} + handleDragAndDrop={handleDragAndDrop} key={weekIndex} week={week} issues={issues} @@ -143,6 +168,7 @@ export const CalendarChart: React.FC = observer((props) => { selectedDate={selectedDate} setSelectedDate={setSelectedDate} issuesFilterStore={issuesFilterStore} + handleDragAndDrop={handleDragAndDrop} week={issueCalendarView.allDaysOfActiveWeek} issues={issues} groupedIssueIds={groupedIssueIds} @@ -175,6 +201,8 @@ export const CalendarChart: React.FC = observer((props) => { addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} + isMonthLayout={false} + showAllIssues isDragDisabled isMobileView /> diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 8f162869a..7dfcfdcac 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -1,10 +1,13 @@ -import { Droppable } from "@hello-pangea/dnd"; +import { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { observer } from "mobx-react-lite"; // types import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; // components import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues"; -// constants +import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; +// helpers import { MONTHS_LIST } from "@/constants/calendar"; // helpers import { cn } from "@/helpers/common.helper"; @@ -24,6 +27,11 @@ type Props = { quickActions: TRenderQuickActions; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; + handleDragAndDrop: ( + issueId: string | undefined, + sourceDate: string | undefined, + destinationDate: string | undefined + ) => Promise; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -51,12 +59,46 @@ export const CalendarDayTile: React.FC = observer((props) => { viewId, readOnly = false, selectedDate, + handleDragAndDrop, setSelectedDate, } = props; + const [isDraggingOver, setIsDraggingOver] = useState(false); + const [showAllIssues, setShowAllIssues] = useState(false); + const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const formattedDatePayload = renderFormattedPayloadDate(date.date); + + const dayTileRef = useRef(null); + + useEffect(() => { + const element = dayTileRef.current; + + if (!element) return; + + return combine( + dropTargetForElements({ + element, + getData: () => ({ date: formattedDatePayload }), + onDragEnter: () => { + setIsDraggingOver(true); + }, + onDragLeave: () => { + setIsDraggingOver(false); + }, + onDrop: ({ source, self }) => { + setIsDraggingOver(false); + const sourceData = source?.data as { id: string; date: string } | undefined; + const destinationData = self?.data as { date: string } | undefined; + handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date); + setShowAllIssues(true); + highlightIssueOnDrop(source?.element?.id, false); + }, + }) + ); + }, [dayTileRef?.current, formattedDatePayload]); + if (!formattedDatePayload) return null; const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; @@ -65,13 +107,19 @@ export const CalendarDayTile: React.FC = observer((props) => { const isToday = date.date.toDateString() === new Date().toDateString(); const isSelectedDate = date.date.toDateString() == selectedDate.toDateString(); + const isWeekend = date.date.getDay() === 0 || date.date.getDay() === 6; + const isMonthLayout = calendarLayout === "month"; + + const normalBackground = isWeekend ? "bg-custom-background-90" : "bg-custom-background-100"; + const draggingOverBackground = isWeekend ? "bg-custom-background-80" : "bg-custom-background-90"; + return ( <> -
+
{/* header */}
= observer((props) => {
{/* content */} -
- - {(provided, snapshot) => ( -
- - {provided.placeholder} -
- )} -
+
+
+ +
{/* Mobile view content */}
setSelectedDate(date.date)} className={cn( - "mx-auto flex h-full w-full cursor-pointer flex-col items-center justify-start py-2.5 text-sm font-medium md:hidden", + "text-sm py-2.5 h-full w-full font-medium mx-auto flex flex-col justify-start items-center md:hidden cursor-pointer opacity-80", { - "bg-custom-background-100": date.date.getDay() !== 0 && date.date.getDay() !== 6, + "bg-custom-background-100": !isWeekend, } )} > diff --git a/web/components/issues/issue-layouts/calendar/issue-block-root.tsx b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx index efedcf50b..df0d03845 100644 --- a/web/components/issues/issue-layouts/calendar/issue-block-root.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx @@ -1,23 +1,54 @@ -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; // components import { TIssueMap } from "@plane/types"; import { CalendarIssueBlock } from "@/components/issues"; +import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { TRenderQuickActions } from "../list/list-view-types"; +import { HIGHLIGHT_CLASS } from "../utils"; // types type Props = { issues: TIssueMap | undefined; issueId: string; quickActions: TRenderQuickActions; - isDragging?: boolean; + isDragDisabled: boolean; }; export const CalendarIssueBlockRoot: React.FC = (props) => { - const { issues, issueId, quickActions, isDragging } = props; + const { issues, issueId, quickActions, isDragDisabled } = props; - if (!issues?.[issueId]) return null; + const issueRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); const issue = issues?.[issueId]; - return ; + useEffect(() => { + const element = issueRef.current; + + if (!element) return; + + return combine( + draggable({ + element, + canDrag: () => !isDragDisabled, + getInitialData: () => ({ id: issue?.id, date: issue?.target_date }), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + }) + ); + }, [issueRef?.current, issue]); + + useOutsideClickDetector(issueRef, () => { + issueRef?.current?.classList?.remove(HIGHLIGHT_CLASS); + }); + + if (!issue) return null; + + return ; }; diff --git a/web/components/issues/issue-layouts/calendar/issue-block.tsx b/web/components/issues/issue-layouts/calendar/issue-block.tsx index 01d7615f3..b9cf59d0a 100644 --- a/web/components/issues/issue-layouts/calendar/issue-block.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-block.tsx @@ -1,4 +1,5 @@ -import { useState, useRef } from "react"; +/* eslint-disable react/display-name */ +import { useState, useRef, forwardRef } from "react"; import { observer } from "mobx-react"; import { MoreHorizontal } from "lucide-react"; import { TIssue } from "@plane/types"; @@ -19,106 +20,111 @@ type Props = { isDragging?: boolean; }; -export const CalendarIssueBlock: React.FC = observer((props) => { - const { issue, quickActions, isDragging = false } = props; - // states - const [isMenuActive, setIsMenuActive] = useState(false); - // refs - const blockRef = useRef(null); - const menuActionRef = useRef(null); - // hooks - const { workspaceSlug, projectId } = useAppRouter(); - const { getProjectIdentifierById } = useProject(); - const { getProjectStates } = useProjectState(); - const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); - const { isMobile } = usePlatformOS(); +export const CalendarIssueBlock = observer( + forwardRef((props, ref) => { + const { issue, quickActions, isDragging = false } = props; + // states + const [isMenuActive, setIsMenuActive] = useState(false); + // refs + const blockRef = useRef(null); + const menuActionRef = useRef(null); + // hooks + const { workspaceSlug, projectId } = useAppRouter(); + const { getProjectIdentifierById } = useProject(); + const { getProjectStates } = useProjectState(); + const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); + const { isMobile } = usePlatformOS(); - const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; + const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; - const handleIssuePeekOverview = (issue: TIssue) => - workspaceSlug && - issue && - issue.project_id && - issue.id && - !getIsIssuePeeked(issue.id) && - setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + !getIsIssuePeeked(issue.id) && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); - useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); - const customActionButton = ( -
setIsMenuActive(!isMenuActive)} - > - -
- ); + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); - const isMenuActionRefAboveScreenBottom = - menuActionRef?.current && menuActionRef?.current?.getBoundingClientRect().bottom < window.innerHeight - 220; + const isMenuActionRefAboveScreenBottom = + menuActionRef?.current && menuActionRef?.current?.getBoundingClientRect().bottom < window.innerHeight - 220; - const placement = isMenuActionRefAboveScreenBottom ? "bottom-end" : "top-end"; + const placement = isMenuActionRefAboveScreenBottom ? "bottom-end" : "top-end"; - return ( - handleIssuePeekOverview(issue)} - className="w-full cursor-pointer text-sm text-custom-text-100" - disabled={!!issue?.tempId} - > - <> - {issue?.tempId !== undefined && ( -
- )} - -
handleIssuePeekOverview(issue)} + className="block w-full text-sm text-custom-text-100 rounded border-b md:border-[1px] border-custom-border-200 hover:border-custom-border-400" + disabled={!!issue?.tempId} + ref={ref} + > + <> + {issue?.tempId !== undefined && ( +
)} - > -
- -
- {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} -
- -
{issue.name}
-
-
+
{ - e.preventDefault(); - e.stopPropagation(); - }} + ref={blockRef} + className={cn( + "group/calendar-block flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded md:px-1 px-4 py-1.5 ", + { + "bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100": isDragging, + "bg-custom-background-100 hover:bg-custom-background-90": !isDragging, + "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id), + } + )} > - {quickActions({ - issue, - parentRef: blockRef, - customActionButton, - placement, - })} +
+ +
+ {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} +
+ +
{issue.name}
+
+
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {quickActions({ + issue, + parentRef: blockRef, + customActionButton, + placement, + })} +
-
- - - ); -}); + + + ); + }) +); + +CalendarIssueBlock.displayName = "CalendarIssueBlock"; diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index 86cecdf20..c04241953 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,5 +1,4 @@ -import { useState } from "react"; -import { Draggable } from "@hello-pangea/dnd"; +import { Dispatch, SetStateAction } from "react"; import { observer } from "mobx-react-lite"; // types import { TIssue, TIssueMap } from "@plane/types"; @@ -14,6 +13,9 @@ type Props = { date: Date; issues: TIssueMap | undefined; issueIdList: string[] | null; + showAllIssues: boolean; + setShowAllIssues?: Dispatch>; + isMonthLayout: boolean; quickActions: TRenderQuickActions; isDragDisabled?: boolean; enableQuickIssueCreate?: boolean; @@ -35,6 +37,8 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { date, issues, issueIdList, + showAllIssues, + setShowAllIssues, quickActions, isDragDisabled = false, enableQuickIssueCreate, @@ -43,10 +47,9 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { addIssuesToView, viewId, readOnly, + isMonthLayout, isMobileView = false, } = props; - // states - const [showAllIssues, setShowAllIssues] = useState(false); const formattedDatePayload = renderFormattedPayloadDate(date); const totalIssues = issueIdList?.length ?? 0; @@ -55,30 +58,27 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { return ( <> - {issueIdList?.slice(0, showAllIssues || isMobileView ? issueIdList.length : 4).map((issueId, index) => - !isMobileView ? ( - - {(provided, snapshot) => ( -
- -
- )} -
- ) : ( - - ) + {issueIdList?.slice(0, showAllIssues || !isMonthLayout ? issueIdList.length : 4).map((issueId) => ( +
+ +
+ ))} + {totalIssues > 4 && isMonthLayout && ( +
+ +
)} - {enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
= observer((props) => { quickAddCallback={quickAddCallback} addIssuesToView={addIssuesToView} viewId={viewId} - onOpen={() => setShowAllIssues(true)} + onOpen={() => setShowAllIssues && setShowAllIssues(true)} />
)} - {totalIssues > 4 && ( -
- -
- )} ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts index fd96ff647..acba413f8 100644 --- a/web/components/issues/issue-layouts/calendar/utils.ts +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -1,36 +1,21 @@ -import { DraggableLocation } from "@hello-pangea/dnd"; -import { TGroupedIssues, IIssueMap, TIssue } from "@plane/types"; +import { TIssue } from "@plane/types"; export const handleDragDrop = async ( - source: DraggableLocation, - destination: DraggableLocation, + issueId: string, + sourceDate: string, + destinationDate: string, workspaceSlug: string | undefined, projectId: string | undefined, - issueMap: IIssueMap, - issueWithIds: TGroupedIssues, updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise ) => { - if (!issueMap || !issueWithIds || !workspaceSlug || !projectId || !updateIssue) return; + if (!workspaceSlug || !projectId || !updateIssue) return; - const sourceColumnId = source?.droppableId || null; - const destinationColumnId = destination?.droppableId || null; + if (sourceDate === destinationDate) return; - if (!workspaceSlug || !projectId || !sourceColumnId || !destinationColumnId) return; + const updatedIssue = { + id: issueId, + target_date: destinationDate, + }; - if (sourceColumnId === destinationColumnId) return; - - // horizontal - if (sourceColumnId != destinationColumnId) { - const sourceIssues = issueWithIds[sourceColumnId] || []; - - const [removed] = sourceIssues.splice(source.index, 1); - const removedIssueDetail = issueMap[removed]; - - const updatedIssue = { - id: removedIssueDetail?.id, - target_date: destinationColumnId, - }; - - return await updateIssue(projectId, updatedIssue.id, updatedIssue); - } + return await updateIssue(projectId, updatedIssue.id, updatedIssue); }; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 0ac4d30b0..9cd107bbd 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -20,6 +20,11 @@ type Props = { quickActions: TRenderQuickActions; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; + handleDragAndDrop: ( + issueId: string | undefined, + sourceDate: string | undefined, + destinationDate: string | undefined + ) => Promise; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -38,6 +43,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { issuesFilterStore, issues, groupedIssueIds, + handleDragAndDrop, week, quickActions, enableQuickIssueCreate, @@ -80,6 +86,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} + handleDragAndDrop={handleDragAndDrop} /> ); })} diff --git a/web/hooks/use-draggable-portal.ts b/web/hooks/use-draggable-portal.ts deleted file mode 100644 index 325f8b268..000000000 --- a/web/hooks/use-draggable-portal.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useRef } from "react"; -import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; -import { createPortal } from "react-dom"; - -const useDraggableInPortal = () => { - const self = useRef(); - - useEffect(() => { - const div = document.createElement("div"); - div.style.position = "absolute"; - div.style.pointerEvents = "none"; - div.style.top = "0"; - div.style.width = "100%"; - div.style.height = "100%"; - self.current = div; - document.body.appendChild(div); - return () => { - document.body.removeChild(div); - }; - }, [self.current]); - - return (render: any) => (provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { - const element = render(provided, snapshot); - if (self.current && snapshot?.isDragging) { - return createPortal(element, self.current); - } - return element; - }; -}; - -export default useDraggableInPortal;