[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(
"name",
"id",
"start_date",
"sequence_id",
"project__name",
"project__identifier",

View File

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

View File

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

View File

@ -1,9 +1,12 @@
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 { differenceInCalendarDays } from "date-fns";
import { observer } from "mobx-react-lite";
// types
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues";
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
@ -91,6 +94,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
setIsDraggingOver(false);
const sourceData = source?.data as { id: string; 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);
setShowAllIssues(true);
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 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 normalBackground = isWeekend ? "bg-custom-background-90" : "bg-custom-background-100";
@ -124,11 +144,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
? "font-medium"
: "text-custom-text-300"
: "font-medium" // if week layout, highlight all days
} ${
date.date.getDay() === 0 || date.date.getDay() === 6
? "bg-custom-background-90"
: "bg-custom-background-100"
} `}
} ${isWeekend ? "bg-custom-background-90" : "bg-custom-background-100"} `}
>
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
{isToday ? (
@ -143,9 +159,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{/* content */}
<div className="h-full w-full hidden md:block">
<div
className={`h-full w-full select-none ${
isDraggingOver ? `${draggingOverBackground} opacity-70` : normalBackground
} ${isMonthLayout ? "min-h-[5rem]" : ""}`}
className={cn(
`h-full w-full select-none ${isDraggingOver ? `${draggingOverBackground} opacity-70` : normalBackground}`,
{
"min-h-[5rem]": isMonthLayout,
}
)}
>
<CalendarIssueBlocks
date={date.date}
@ -177,7 +196,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
)}
>
<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/10 text-custom-primary-100 ": isToday && !isSelectedDate,
})}
@ -185,7 +204,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{date.date.getDate()}
</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>
</>

View File

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