mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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:
parent
f9de1e790c
commit
4c16ed8b23
@ -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",
|
||||||
|
1
packages/types/src/project/projects.d.ts
vendored
1
packages/types/src/project/projects.d.ts
vendored
@ -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;
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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 && (
|
||||||
|
Loading…
Reference in New Issue
Block a user