[WEB-1336] fix: issue dates conflict in the calendar layout (#4480)

* fix: calendar dnd for due dates before issue start date

* chore: start date in calender view

* fix: add existing issues to calendar layout

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-05-17 12:45:28 +05:30 committed by GitHub
parent f9de1e790c
commit 4c16ed8b23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 30 deletions

View File

@ -289,6 +289,7 @@ class IssueSearchEndpoint(BaseAPIView):
issues.values( issues.values(
"name", "name",
"id", "id",
"start_date",
"sequence_id", "sequence_id",
"project__name", "project__name",
"project__identifier", "project__identifier",

View File

@ -147,6 +147,7 @@ export interface ISearchIssueResponse {
project__identifier: string; project__identifier: string;
project__name: string; project__name: string;
sequence_id: number; sequence_id: number;
start_date: string | null;
state__color: string; state__color: string;
state__group: TStateGroups; state__group: TStateGroups;
state__name: string; state__name: string;

View File

@ -1,17 +1,17 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Rocket, Search, X } from "lucide-react"; import { Rocket, Search, X } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox, Dialog, Transition } from "@headlessui/react";
// types
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
// services // ui
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import useDebounce from "@/hooks/use-debounce"; import useDebounce from "@/hooks/use-debounce";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { ProjectService } from "@/services/project"; import { ProjectService } from "@/services/project";
// hooks
// components // components
import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state"; import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state";
// ui
// types
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -21,6 +21,7 @@ type Props = {
searchParams: Partial<TProjectIssuesSearchParams>; searchParams: Partial<TProjectIssuesSearchParams>;
handleOnSubmit: (data: ISearchIssueResponse[]) => Promise<void>; handleOnSubmit: (data: ISearchIssueResponse[]) => Promise<void>;
workspaceLevelToggle?: boolean; workspaceLevelToggle?: boolean;
shouldHideIssue?: (issue: ISearchIssueResponse) => boolean;
}; };
const projectService = new ProjectService(); const projectService = new ProjectService();
@ -34,6 +35,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
searchParams, searchParams,
handleOnSubmit, handleOnSubmit,
workspaceLevelToggle = false, workspaceLevelToggle = false,
shouldHideIssue,
} = props; } = props;
// states // states
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -87,6 +89,8 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
}); });
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]); }, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]);
const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue));
return ( return (
<> <>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear> <Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
@ -207,16 +211,16 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
</Loader> </Loader>
) : ( ) : (
<> <>
{issues.length === 0 ? ( {filteredIssues.length === 0 ? (
<IssueSearchModalEmptyState <IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm} debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching} isSearching={isSearching}
issues={issues} issues={filteredIssues}
searchTerm={searchTerm} searchTerm={searchTerm}
/> />
) : ( ) : (
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}> <ul className={`text-sm text-custom-text-100 ${filteredIssues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => { {filteredIssues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id); const selected = selectedIssues.some((i) => i.id === issue.id);
return ( return (

View File

@ -1,9 +1,12 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { differenceInCalendarDays } from "date-fns";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types // types
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues"; import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues";
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
@ -91,6 +94,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
setIsDraggingOver(false); setIsDraggingOver(false);
const sourceData = source?.data as { id: string; date: string } | undefined; const sourceData = source?.data as { id: string; date: string } | undefined;
const destinationData = self?.data as { date: string } | undefined; const destinationData = self?.data as { date: string } | undefined;
if (!sourceData || !destinationData) return;
const issueDetails = issues?.[sourceData?.id];
if (issueDetails?.start_date) {
const issueStartDate = new Date(issueDetails.start_date);
const targetDate = new Date(destinationData?.date);
const diffInDays = differenceInCalendarDays(targetDate, issueStartDate);
if (diffInDays < 0) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Due date cannot be before the start date of the issue.",
});
return;
}
}
handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date); handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date);
setShowAllIssues(true); setShowAllIssues(true);
highlightIssueOnDrop(source?.element?.id, false); highlightIssueOnDrop(source?.element?.id, false);
@ -107,7 +127,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
const isToday = date.date.toDateString() === new Date().toDateString(); const isToday = date.date.toDateString() === new Date().toDateString();
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString(); const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
const isWeekend = date.date.getDay() === 0 || date.date.getDay() === 6; const isWeekend = [0, 6].includes(date.date.getDay());
const isMonthLayout = calendarLayout === "month"; const isMonthLayout = calendarLayout === "month";
const normalBackground = isWeekend ? "bg-custom-background-90" : "bg-custom-background-100"; const normalBackground = isWeekend ? "bg-custom-background-90" : "bg-custom-background-100";
@ -124,11 +144,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
? "font-medium" ? "font-medium"
: "text-custom-text-300" : "text-custom-text-300"
: "font-medium" // if week layout, highlight all days : "font-medium" // if week layout, highlight all days
} ${ } ${isWeekend ? "bg-custom-background-90" : "bg-custom-background-100"} `}
date.date.getDay() === 0 || date.date.getDay() === 6
? "bg-custom-background-90"
: "bg-custom-background-100"
} `}
> >
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
{isToday ? ( {isToday ? (
@ -143,9 +159,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{/* content */} {/* content */}
<div className="h-full w-full hidden md:block"> <div className="h-full w-full hidden md:block">
<div <div
className={`h-full w-full select-none ${ className={cn(
isDraggingOver ? `${draggingOverBackground} opacity-70` : normalBackground `h-full w-full select-none ${isDraggingOver ? `${draggingOverBackground} opacity-70` : normalBackground}`,
} ${isMonthLayout ? "min-h-[5rem]" : ""}`} {
"min-h-[5rem]": isMonthLayout,
}
)}
> >
<CalendarIssueBlocks <CalendarIssueBlocks
date={date.date} date={date.date}
@ -177,7 +196,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
)} )}
> >
<div <div
className={cn("flex h-6 w-6 items-center justify-center rounded-full ", { className={cn("size-6 flex items-center justify-center rounded-full", {
"bg-custom-primary-100 text-white": isSelectedDate, "bg-custom-primary-100 text-white": isSelectedDate,
"bg-custom-primary-100/10 text-custom-primary-100 ": isToday && !isSelectedDate, "bg-custom-primary-100/10 text-custom-primary-100 ": isToday && !isSelectedDate,
})} })}
@ -185,7 +204,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{date.date.getDate()} {date.date.getDate()}
</div> </div>
{totalIssues > 0 && <div className="mt-1 flex h-1.5 w-1.5 flex-shrink-0 rounded bg-custom-primary-100" />} {totalIssues > 0 && <div className="mt-1 size-1.5 flex flex-shrink-0 rounded bg-custom-primary-100" />}
</div> </div>
</div> </div>
</> </>

View File

@ -1,25 +1,24 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { differenceInCalendarDays } from "date-fns";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// components
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// types
import { ISearchIssueResponse, TIssue } from "@plane/types"; import { ISearchIssueResponse, TIssue } from "@plane/types";
// ui
import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui"; import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core"; import { ExistingIssuesListModal } from "@/components/core";
// hooks // constants
import { ISSUE_CREATED } from "@/constants/event-tracker"; import { ISSUE_CREATED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { createIssuePayload } from "@/helpers/issue.helper"; import { createIssuePayload } from "@/helpers/issue.helper";
// hooks
import { useEventTracker, useIssueDetail, useProject } from "@/hooks/store"; import { useEventTracker, useIssueDetail, useProject } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress"; import useKeypress from "@/hooks/use-keypress";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// helpers
// icons
// ui
// types
// constants
// helper
type Props = { type Props = {
formKey: keyof TIssue; formKey: keyof TIssue;
@ -182,9 +181,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})
) )
); );
if (addIssuesToView) { await addIssuesToView?.(issueIds);
await addIssuesToView(issueIds);
}
} catch (error) { } catch (error) {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -212,6 +209,15 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
handleClose={() => setIsExistingIssueModalOpen(false)} handleClose={() => setIsExistingIssueModalOpen(false)}
searchParams={ExistingIssuesListModalPayload} searchParams={ExistingIssuesListModalPayload}
handleOnSubmit={handleAddIssuesToView} handleOnSubmit={handleAddIssuesToView}
shouldHideIssue={(issue) => {
if (issue.start_date && prePopulatedData?.target_date) {
const issueStartDate = new Date(issue.start_date);
const targetDate = new Date(prePopulatedData.target_date);
const diffInDays = differenceInCalendarDays(targetDate, issueStartDate);
if (diffInDays < 0) return true;
}
return false;
}}
/> />
)} )}
{isOpen && ( {isOpen && (