diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index efd785d3e..96d915149 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,9 +1,11 @@ +import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import useSize from "hooks/use-window-size"; // components // ui import { Spinner } from "@plane/ui"; -import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; +import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types import { IIssueDisplayFilterOptions, @@ -15,6 +17,9 @@ import { TIssueMap, } from "@plane/types"; import { ICalendarWeek } from "./types"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; @@ -24,6 +29,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { MONTHS_LIST } from "constants/calendar"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -62,6 +68,8 @@ export const CalendarChart: React.FC = observer((props) => { updateFilters, readOnly = false, } = props; + // states + const [selectedDate, setSelectedDate] = useState(new Date()); // store hooks const { issues: { viewFlags }, @@ -70,6 +78,7 @@ export const CalendarChart: React.FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); + const [windowWidth] = useSize(); const { enableIssueCreation } = viewFlags || {}; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -78,18 +87,30 @@ export const CalendarChart: React.FC = observer((props) => { const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth; - if (!calendarPayload) + const formattedDatePayload = renderFormattedPayloadDate(selectedDate) ?? undefined; + + if (!calendarPayload || !formattedDatePayload) return (
); + const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; + return ( <>
- -
+ +
768, + })} + >
{layout === "month" && ( @@ -97,6 +118,8 @@ export const CalendarChart: React.FC = observer((props) => { {allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( = observer((props) => { )} {layout === "week" && ( = observer((props) => { /> )}
+ + {/* mobile view */} +
+

+ {`${selectedDate.getDate()} ${ + MONTHS_LIST[selectedDate.getMonth() + 1].title + }, ${selectedDate.getFullYear()}`} +

+ +
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 8ac1e460c..12b7f767b 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; import { Droppable } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; // components -import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; +import { CalendarIssueBlocks, ICalendarDate } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; // types @@ -31,6 +31,8 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + selectedDate: Date; + setSelectedDate: (date: Date) => void; }; export const CalendarDayTile: React.FC = observer((props) => { @@ -46,8 +48,10 @@ export const CalendarDayTile: React.FC = observer((props) => { addIssuesToView, viewId, readOnly = false, + selectedDate, + setSelectedDate, } = props; - const [showAllIssues, setShowAllIssues] = useState(false); + const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const formattedDatePayload = renderFormattedPayloadDate(date.date); @@ -57,13 +61,14 @@ export const CalendarDayTile: React.FC = observer((props) => { const totalIssues = issueIdList?.length ?? 0; const isToday = date.date.toDateString() === new Date().toDateString(); + const isSelectedDate = date.date.toDateString() == selectedDate.toDateString(); return ( <>
{/* header */}
= observer((props) => {
{/* content */} -
+
{(provided, snapshot) => (
= observer((props) => { ref={provided.innerRef} > - - {enableQuickIssueCreate && !disableIssueCreation && !readOnly && ( -
- setShowAllIssues(true)} - /> -
- )} - - {totalIssues > 4 && ( -
- -
- )} - {provided.placeholder}
)}
+ + {/* Mobile view content */} +
setSelectedDate(date.date)} + className={cn( + "text-sm py-2.5 h-full w-full font-medium mx-auto flex flex-col justify-start items-center md:hidden cursor-pointer", + { + "bg-custom-background-100": date.date.getDay() !== 0 && date.date.getDay() !== 6, + } + )} + > +
+ {date.date.getDate()} +
+ + {totalIssues > 0 &&
} +
); diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 3050bba72..953c8384f 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -4,9 +4,10 @@ import { useRouter } from "next/router"; import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; // hooks +import useSize from "hooks/use-window-size"; // ui // icons -import { Check, ChevronUp } from "lucide-react"; +import { Check, ChevronUp, MoreVerticalIcon } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types import { @@ -41,6 +42,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const { projectId } = router.query; const issueCalendarView = useCalendarView(); + const [windowWidth] = useSize(); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -60,7 +62,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; - const handleLayoutChange = (layout: TCalendarLayouts) => { + const handleLayoutChange = (layout: TCalendarLayouts, closePopover: any) => { if (!projectId || !updateFilters) return; updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { @@ -75,6 +77,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop ? issueCalendarView.calendarFilters.activeMonthDate : issueCalendarView.calendarFilters.activeWeekDate ); + if (windowWidth <= 768) closePopover(); // close the popover on mobile }; const handleToggleWeekends = () => { @@ -92,21 +95,24 @@ export const CalendarOptionsDropdown: React.FC = observer((prop return ( - {({ open }) => ( + {({ open, close: closePopover }) => ( <> - @@ -132,7 +138,7 @@ export const CalendarOptionsDropdown: React.FC = observer((prop key={layout} type="button" className="flex w-full items-center justify-between gap-2 rounded px-1 py-1.5 text-left text-xs hover:bg-custom-background-80" - onClick={() => handleLayoutChange(layoutDetails.key)} + onClick={() => handleLayoutChange(layoutDetails.key, closePopover)} > {layoutDetails.title} {calendarLayout === layout && } @@ -144,7 +150,12 @@ export const CalendarOptionsDropdown: React.FC = observer((prop onClick={handleToggleWeekends} > Show weekends - {}} /> + { + if (windowWidth <= 768) closePopover(); // close the popover on mobile + }} + />
diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index aa055534d..bb3bc3c6d 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -24,10 +24,11 @@ interface ICalendarHeader { filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters ) => Promise; + setSelectedDate: (date: Date) => void; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore, updateFilters } = props; + const { issuesFilterStore, updateFilters, setSelectedDate } = props; const issueCalendarView = useCalendarView(); @@ -91,6 +92,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeMonthDate: firstDayOfCurrentMonth, activeWeekDate: today, }); + setSelectedDate(today); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index 69fce6662..527a9eff4 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -5,6 +5,8 @@ export * from "./types.d"; export * from "./day-tile"; export * from "./header"; export * from "./issue-blocks"; +export * from "./issue-block-root"; +export * from "./issue-block"; export * from "./week-days"; export * from "./week-header"; export * from "./quick-add-issue-form"; diff --git a/web/components/issues/issue-layouts/calendar/issue-block-root.tsx b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx new file mode 100644 index 000000000..1f2f84869 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/issue-block-root.tsx @@ -0,0 +1,22 @@ +import React from "react"; +// components +import { CalendarIssueBlock } from "components/issues"; +// types +import { TIssue, TIssueMap } from "@plane/types"; + +type Props = { + issues: TIssueMap | undefined; + issueId: string; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; + isDragging?: boolean; +}; + +export const CalendarIssueBlockRoot: React.FC = (props) => { + const { issues, issueId, quickActions, isDragging } = props; + + if (!issues?.[issueId]) return null; + + const issue = issues?.[issueId]; + + return ; +}; diff --git a/web/components/issues/issue-layouts/calendar/issue-block.tsx b/web/components/issues/issue-layouts/calendar/issue-block.tsx new file mode 100644 index 000000000..226d447f2 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/issue-block.tsx @@ -0,0 +1,113 @@ +import { useState, useRef } from "react"; +import { MoreHorizontal } from "lucide-react"; +import { observer } from "mobx-react"; +// components +import { Tooltip, ControlLink } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TIssue } from "@plane/types"; +import { usePlatformOS } from "hooks/use-platform-os"; + +type Props = { + issue: TIssue; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; + isDragging?: boolean; +}; + +export const CalendarIssueBlock: React.FC = observer((props) => { + const { issue, quickActions, isDragging = false } = props; + // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { getProjectIdentifierById } = useProject(); + const { getProjectStates } = useProjectState(); + const { peekIssue, setPeekIssue } = useIssueDetail(); + const { isMobile } = usePlatformOS(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + + const menuActionRef = useRef(null); + + 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 && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + + return ( + handleIssuePeekOverview(issue)} + className="w-full cursor-pointer text-sm text-custom-text-100" + disabled={!!issue?.tempId} + > + <> + {issue?.tempId !== undefined && ( +
+ )} + +
+
+ +
+ {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} +
+ +
{issue.name}
+
+
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {quickActions(issue, customActionButton)} +
+
+ + + ); +}); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index df183ba3d..0cb4e572c 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,74 +1,62 @@ -import { useState, useRef } from "react"; +import { useState } from "react"; import { Draggable } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; -import { MoreHorizontal } from "lucide-react"; // components -import { Tooltip, ControlLink } from "@plane/ui"; -// hooks -import { cn } from "helpers/common.helper"; -import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { usePlatformOS } from "hooks/use-platform-os"; +import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "components/issues"; // helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TIssue, TIssueMap } from "@plane/types"; +import useSize from "hooks/use-window-size"; type Props = { + date: Date; issues: TIssueMap | undefined; issueIdList: string[] | null; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - showAllIssues?: boolean; isDragDisabled?: boolean; + enableQuickIssueCreate?: boolean; + disableIssueCreation?: boolean; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: TIssue, + viewId?: string + ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; + viewId?: string; + readOnly?: boolean; }; export const CalendarIssueBlocks: React.FC = observer((props) => { - const { issues, issueIdList, quickActions, showAllIssues = false, isDragDisabled = false } = props; - // hooks const { - router: { workspaceSlug, projectId }, - } = useApplication(); - const { getProjectIdentifierById } = useProject(); - const { getProjectStates } = useProjectState(); - const { peekIssue, setPeekIssue } = useIssueDetail(); - const { isMobile } = usePlatformOS(); + date, + issues, + issueIdList, + quickActions, + isDragDisabled = false, + enableQuickIssueCreate, + disableIssueCreation, + quickAddCallback, + addIssuesToView, + viewId, + readOnly, + } = props; // states - const [isMenuActive, setIsMenuActive] = useState(false); + const [showAllIssues, setShowAllIssues] = useState(false); + // hooks + const [windowWidth] = useSize(); - const menuActionRef = useRef(null); + const formattedDatePayload = renderFormattedPayloadDate(date); + const totalIssues = issueIdList?.length ?? 0; - const handleIssuePeekOverview = (issue: TIssue) => - workspaceSlug && - issue && - issue.project_id && - issue.id && - setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); - - useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); - - const customActionButton = ( -
setIsMenuActive(!isMenuActive)} - > - -
- ); + if (!formattedDatePayload) return null; return ( <> - {issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => { - if (!issues?.[issueId]) return null; - - const issue = issues?.[issueId]; - - const stateColor = - getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; - - return ( - + {issueIdList?.slice(0, showAllIssues || windowWidth <= 768 ? issueIdList.length : 4).map((issueId, index) => + windowWidth > 768 ? ( + {(provided, snapshot) => (
= observer((props) => { {...provided.dragHandleProps} ref={provided.innerRef} > - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - disabled={!!issue?.tempId} - > - <> - {issue?.tempId !== undefined && ( -
- )} - -
-
- -
- {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id} -
- -
{issue.name}
-
-
-
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {quickActions(issue, customActionButton)} -
-
- - +
)} - ); - })} + ) : ( + + ) + )} + + {enableQuickIssueCreate && !disableIssueCreation && !readOnly && ( +
+ setShowAllIssues(true)} + /> +
+ )} + {totalIssues > 4 && ( +
+ +
+ )} ); }); diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 5f62706dc..3b319fdad 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -50,7 +50,7 @@ const Inputs = (props: any) => { return ( <> -

{projectDetails?.identifier ?? "..."}

+

{projectDetails?.identifier ?? "..."}

{ {...register("name", { required: "Issue title is required.", })} - className="w-full rounded-md bg-transparent py-1.5 pr-2 text-xs font-medium leading-5 text-custom-text-200 outline-none" + className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none" /> ); @@ -221,7 +221,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { >
@@ -230,7 +230,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { {!isOpen && (
diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index ec1d12e59..968ae4097 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -28,6 +28,8 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + selectedDate: Date; + setSelectedDate: (date: Date) => void; }; export const CalendarWeekDays: React.FC = observer((props) => { @@ -43,6 +45,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { addIssuesToView, viewId, readOnly = false, + selectedDate, + setSelectedDate, } = props; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; @@ -52,7 +56,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { return (
@@ -61,6 +65,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { return ( = observer((props) => { return (
@@ -24,7 +24,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; return ( -
+
{day.shortTitle}
);