feat: calendar view display properties (#1024)

* fix: calendar mutation fix, chore: calendar type added

* feat: calendar view display property added

* feat: calendar header, single date and single issue component added, chore: code refactor

* chore: partialupdateissue function updated

* fix: dropdown overflow fix, style: issue card styling

* fix: calendar weekly view row height fix

* feat: calendar issue card ellipsis added, fix: edit and delete mutation fix

* style: plus icon , add issue button padding and onhover effect fix

* style: calendar issue card

* style: weekly view height
This commit is contained in:
Anmol Singh Bhatia 2023-05-11 02:27:14 +05:30 committed by GitHub
parent 4f2b106852
commit c7deb00f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 803 additions and 436 deletions

View File

@ -98,7 +98,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
(formData: Partial<IIssue>, issueId: string) => {
if (!workspaceSlug || !projectId) return;
if (cycleId)
@ -164,7 +164,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
}
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
.then(() => {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
@ -178,18 +178,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
console.log(error);
});
},
[
workspaceSlug,
projectId,
cycleId,
moduleId,
issue,
groupTitle,
index,
selectedGroup,
orderBy,
params,
]
[workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
);
const getStyle = (

View File

@ -0,0 +1,217 @@
import { Popover, Transition } from "@headlessui/react";
import {
addMonths,
addSevenDaysToDate,
formatDate,
getCurrentWeekEndDate,
getCurrentWeekStartDate,
isSameMonth,
isSameYear,
lastDayOfWeek,
startOfWeek,
subtract7DaysToDate,
subtractMonths,
updateDateWithMonth,
updateDateWithYear,
} from "helpers/calendar.helper";
import React from "react";
import { monthOptions, yearOptions } from "constants/calendar";
import { ICalendarRange } from "types";
import {
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "@heroicons/react/24/outline";
import { CustomMenu, ToggleSwitch } from "components/ui";
type Props = {
isMonthlyView: boolean;
setIsMonthlyView: React.Dispatch<React.SetStateAction<boolean>>;
currentDate: Date;
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
setCalendarDateRange: React.Dispatch<React.SetStateAction<ICalendarRange>>;
showWeekEnds: boolean;
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
};
export const CalendarHeader: React.FC<Props> = ({
setIsMonthlyView,
isMonthlyView,
currentDate,
setCurrentDate,
setCalendarDateRange,
showWeekEnds,
setShowWeekEnds,
}) => {
const updateDate = (date: Date) => {
setCurrentDate(date);
setCalendarDateRange({
startDate: startOfWeek(date),
endDate: lastDayOfWeek(date),
});
};
return (
<div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg">
{({ open }) => (
<>
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
<span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span>
</div>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{yearOptions.map((year) => (
<button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${
isSameYear(year.value, currentDate)
? "text-sm font-medium text-brand-base"
: "text-xs text-brand-secondary "
} hover:text-sm hover:font-medium hover:text-brand-base`}
>
{year.label}
</button>
))}
</div>
<div className="grid grid-cols-4 border-t border-brand-base px-2">
{monthOptions.map((month) => (
<button
onClick={() => updateDate(updateDateWithMonth(month.value, currentDate))}
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
isSameMonth(month.value, currentDate) ? "font-medium text-brand-base" : ""
}`}
>
{month.label}
</button>
))}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<div className="flex items-center gap-2">
<button
className="cursor-pointer"
onClick={() => {
if (isMonthlyView) {
updateDate(subtractMonths(currentDate, 1));
} else {
setCurrentDate(subtract7DaysToDate(currentDate));
setCalendarDateRange({
startDate: getCurrentWeekStartDate(subtract7DaysToDate(currentDate)),
endDate: getCurrentWeekEndDate(subtract7DaysToDate(currentDate)),
});
}
}}
>
<ChevronLeftIcon className="h-4 w-4" />
</button>
<button
className="cursor-pointer"
onClick={() => {
if (isMonthlyView) {
updateDate(addMonths(currentDate, 1));
} else {
setCurrentDate(addSevenDaysToDate(currentDate));
setCalendarDateRange({
startDate: getCurrentWeekStartDate(addSevenDaysToDate(currentDate)),
endDate: getCurrentWeekEndDate(addSevenDaysToDate(currentDate)),
});
}
}}
>
<ChevronRightIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="flex w-full items-center justify-end gap-2">
<button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
onClick={() => {
if (isMonthlyView) {
updateDate(new Date());
} else {
setCurrentDate(new Date());
setCalendarDateRange({
startDate: getCurrentWeekStartDate(new Date()),
endDate: getCurrentWeekEndDate(new Date()),
});
}
}}
>
Today
</button>
<CustomMenu
customButton={
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
{isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
>
<CustomMenu.MenuItem
onClick={() => {
setIsMonthlyView(true);
setCalendarDateRange({
startDate: startOfWeek(currentDate),
endDate: lastDayOfWeek(currentDate),
});
}}
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
<span className="flex items-center gap-2">Monthly View</span>
<CheckIcon
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-100" : "opacity-0"}`}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setIsMonthlyView(false);
setCalendarDateRange({
startDate: getCurrentWeekStartDate(currentDate),
endDate: getCurrentWeekEndDate(currentDate),
});
}}
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full items-center justify-between gap-2">
<span className="flex items-center gap-2">Weekly View</span>
<CheckIcon
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-0" : "opacity-100"}`}
/>
</div>
</CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
<h4>Show weekends</h4>
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
</div>
</CustomMenu>
</div>
</div>
);
};
export default CalendarHeader;

View File

@ -1,8 +1,27 @@
import React, { useState } from "react";
// swr
import useSWR, { mutate } from "swr";
import Link from "next/link";
import { useRouter } from "next/router";
// ui
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import { SingleCalendarDate, CalendarHeader } from "components/core";
import { Spinner } from "components/ui";
// hooks
import useIssuesView from "hooks/use-issues-view";
// services
import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
// fetch key
import {
CYCLE_CALENDAR_ISSUES,
MODULE_CALENDAR_ISSUES,
PROJECT_CALENDAR_ISSUES,
} from "constants/fetch-keys";
// helper
import { renderDateFormat } from "helpers/date-time.helper";
import {
@ -11,62 +30,28 @@ import {
eachDayOfInterval,
weekDayInterval,
formatDate,
getCurrentWeekStartDate,
getCurrentWeekEndDate,
subtractMonths,
addMonths,
updateDateWithYear,
updateDateWithMonth,
isSameMonth,
isSameYear,
subtract7DaysToDate,
addSevenDaysToDate,
} from "helpers/calendar.helper";
// ui
import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
// icon
import {
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// hooks
import useIssuesView from "hooks/use-issues-view";
// services
import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service";
// fetch key
import {
CYCLE_CALENDAR_ISSUES,
MODULE_CALENDAR_ISSUES,
PROJECT_CALENDAR_ISSUES,
} from "constants/fetch-keys";
// type
import { IIssue } from "types";
// constant
import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
import { ICalendarRange, IIssue, UserAuth } from "types";
type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
addIssueToDate: (date: string) => void;
isCompleted: boolean;
userAuth: UserAuth;
};
interface ICalendarRange {
startDate: Date;
endDate: Date;
}
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
export const CalendarView: React.FC<Props> = ({
handleEditIssue,
handleDeleteIssue,
addIssueToDate,
isCompleted = false,
userAuth,
}) => {
const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -78,12 +63,6 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
endDate: lastDayOfWeek(currentDate),
});
const targetDateFilter = {
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
calendarDateRange.endDate
)};before`,
};
const { data: projectCalendarIssues } = useSWR(
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
workspaceSlug && projectId
@ -218,186 +197,21 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
});
};
const updateDate = (date: Date) => {
setCurrentDate(date);
setCalendarDateRange({
startDate: startOfWeek(date),
endDate: lastDayOfWeek(date),
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
return calendarIssues ? (
<div className="mb-4 h-full">
<DragDropContext onDragEnd={onDragEnd}>
<div className="-m-2 h-full overflow-y-auto rounded-lg p-8 text-brand-secondary">
<div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg">
{({ open }) => (
<>
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
<span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span>
</div>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{yearOptions.map((year) => (
<button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${
isSameYear(year.value, currentDate)
? "text-sm font-medium text-brand-base"
: "text-xs text-brand-secondary "
} hover:text-sm hover:font-medium hover:text-brand-base`}
>
{year.label}
</button>
))}
</div>
<div className="grid grid-cols-4 border-t border-brand-base px-2">
{monthOptions.map((month) => (
<button
onClick={() =>
updateDate(updateDateWithMonth(month.value, currentDate))
}
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
isSameMonth(month.value, currentDate)
? "font-medium text-brand-base"
: ""
}`}
>
{month.label}
</button>
))}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<div className="flex items-center gap-2">
<button
className="cursor-pointer"
onClick={() => {
if (isMonthlyView) {
updateDate(subtractMonths(currentDate, 1));
} else {
setCurrentDate(subtract7DaysToDate(currentDate));
setCalendarDateRange({
startDate: getCurrentWeekStartDate(subtract7DaysToDate(currentDate)),
endDate: getCurrentWeekEndDate(subtract7DaysToDate(currentDate)),
});
}
}}
>
<ChevronLeftIcon className="h-4 w-4" />
</button>
<button
className="cursor-pointer"
onClick={() => {
if (isMonthlyView) {
updateDate(addMonths(currentDate, 1));
} else {
setCurrentDate(addSevenDaysToDate(currentDate));
setCalendarDateRange({
startDate: getCurrentWeekStartDate(addSevenDaysToDate(currentDate)),
endDate: getCurrentWeekEndDate(addSevenDaysToDate(currentDate)),
});
}
}}
>
<ChevronRightIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="flex w-full items-center justify-end gap-2">
<button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
onClick={() => {
if (isMonthlyView) {
updateDate(new Date());
} else {
setCurrentDate(new Date());
setCalendarDateRange({
startDate: getCurrentWeekStartDate(new Date()),
endDate: getCurrentWeekEndDate(new Date()),
});
}
}}
>
Today
</button>
<CustomMenu
customButton={
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
{isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
>
<CustomMenu.MenuItem
onClick={() => {
setIsMonthlyView(true);
setCalendarDateRange({
startDate: startOfWeek(currentDate),
endDate: lastDayOfWeek(currentDate),
});
}}
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
<span className="flex items-center gap-2">Monthly View</span>
<CheckIcon
className={`h-4 w-4 flex-shrink-0 ${
isMonthlyView ? "opacity-100" : "opacity-0"
}`}
<div className="-m-2 h-full rounded-lg p-8 text-brand-secondary">
<CalendarHeader
isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView}
showWeekEnds={showWeekEnds}
setShowWeekEnds={setShowWeekEnds}
currentDate={currentDate}
setCurrentDate={setCurrentDate}
setCalendarDateRange={setCalendarDateRange}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setIsMonthlyView(false);
setCalendarDateRange({
startDate: getCurrentWeekStartDate(currentDate),
endDate: getCurrentWeekEndDate(currentDate),
});
}}
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full items-center justify-between gap-2">
<span className="flex items-center gap-2">Weekly View</span>
<CheckIcon
className={`h-4 w-4 flex-shrink-0 ${
isMonthlyView ? "opacity-0" : "opacity-100"
}`}
/>
</div>
</CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
<h4>Show weekends</h4>
<ToggleSwitch
value={showWeekEnds}
onChange={() => setShowWeekEnds(!showWeekEnds)}
/>
</div>
</CustomMenu>
</div>
</div>
<div
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
@ -420,7 +234,9 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
}`}
>
<span>
{isMonthlyView ? formatDate(date, "eee").substring(0, 3) : formatDate(date, "eee")}
{isMonthlyView
? formatDate(date, "eee").substring(0, 3)
: formatDate(date, "eee")}
</span>
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
</div>
@ -428,94 +244,26 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
</div>
<div
className={`grid h-full auto-rows-[minmax(150px,1fr)] ${
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => {
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{totalIssues > 0 &&
date.issues
.slice(0, showAllIssues ? totalIssues : 4)
.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
>
<a className="flex w-full items-center gap-2">
{getStateGroupIcon(
issue.state_detail.group,
"12",
"12",
issue.state_detail.color
)}
{issue.name}
</a>
</Link>
</div>
)}
</Draggable>
{currentViewDaysData.map((date, index) => (
<SingleCalendarDate
index={index}
date={date}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
addIssueToDate={addIssueToDate}
isMonthlyView={isMonthlyView}
showWeekEnds={showWeekEnds}
isNotAllowed={isNotAllowed}
/>
))}
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute ${
isMonthlyView ? "bottom-2" : "top-2"
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
>
<PlusIcon className="h-3 w-3 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
);
})}
</div>
</div>
</DragDropContext>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />

View File

@ -1 +1,4 @@
export * from "./calendar"
export * from "./calendar";
export * from "./single-date";
export * from "./single-issue";
export * from "./calendar-header";

View File

@ -0,0 +1,107 @@
import React, { useState } from "react";
// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
// component
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { SingleCalendarIssue } from "./single-issue";
// icons
import { PlusSmallIcon } from "@heroicons/react/24/outline";
// helper
import { formatDate } from "helpers/calendar.helper";
// types
import { IIssue } from "types";
type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
index: number;
date: {
date: string;
issues: IIssue[];
};
addIssueToDate: (date: string) => void;
isMonthlyView: boolean;
showWeekEnds: boolean;
isNotAllowed: boolean;
};
export const SingleCalendarDate: React.FC<Props> = ({
handleEditIssue,
handleDeleteIssue,
date,
index,
addIssueToDate,
isMonthlyView,
showWeekEnds,
isNotAllowed,
}) => {
const [showAllIssues, setShowAllIssues] = useState(false);
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
isMonthlyView ? "" : "pt-9"
} ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{totalIssues > 0 &&
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
<Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<SingleCalendarIssue
key={index}
index={index}
provided={provided}
snapshot={snapshot}
issue={issue}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
isNotAllowed={isNotAllowed}
/>
)}
</Draggable>
))}
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
>
<PlusSmallIcon className="h-4 w-4 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
);
};

View File

@ -0,0 +1,262 @@
import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
// services
import issuesService from "services/issues.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useToast from "hooks/use-toast";
// components
import { CustomMenu, Tooltip } from "components/ui";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues";
// icons
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
// helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// type
import { IIssue } from "types";
// fetch-keys
import {
CYCLE_CALENDAR_ISSUES,
MODULE_CALENDAR_ISSUES,
PROJECT_CALENDAR_ISSUES,
} from "constants/fetch-keys";
type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
index: number;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
issue: IIssue;
isNotAllowed: boolean;
};
export const SingleCalendarIssue: React.FC<Props> = ({
handleEditIssue,
handleDeleteIssue,
index,
provided,
snapshot,
issue,
isNotAllowed,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { setToastAlert } = useToast();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => {
if (!workspaceSlug || !projectId) return;
const fetchKey = cycleId
? CYCLE_CALENDAR_ISSUES(projectId as string, cycleId as string)
: moduleId
? MODULE_CALENDAR_ISSUES(projectId as string, moduleId as string)
: PROJECT_CALENDAR_ISSUES(projectId as string);
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issueId)
return {
...p,
formData,
};
return p;
}),
false
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData)
.then(() => {
mutate<IIssue[]>(fetchKey);
})
.catch((error) => {
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId]
);
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const displayProperties = properties
? Object.values(properties).some((value) => value === true)
: false;
return (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer rounded border border-brand-base px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
}`}
>
<div className="group/card relative flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto ">
{!isNotAllowed && (
<div className="z-1 absolute top-1.5 right-1.5 opacity-0 group-hover/card:opacity-100">
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={() => handleEditIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
)}
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}>
<a className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-xs text-brand-base">{truncateText(issue.name, 25)}</span>
</Tooltip>
</a>
</Link>
{displayProperties && (
<div className="relative mt-1.5 flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && issue.label_details.length > 0 ? (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
>
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
) : (
""
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
isNotAllowed={isNotAllowed}
/>
)}
{properties.link && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
</div>
)}
</div>
</div>
);
};

View File

@ -1,4 +1,5 @@
export * from "./board-view";
export * from "./calendar-view";
export * from "./list-view";
export * from "./sidebar";
export * from "./bulk-delete-issues-modal";

View File

@ -233,7 +233,7 @@ export const IssuesFilterView: React.FC = () => {
</>
)}
</div>
{issueView !== "calendar" && (
<div className="space-y-2 py-3">
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2">
@ -257,7 +257,6 @@ export const IssuesFilterView: React.FC = () => {
})}
</div>
</div>
)}
</div>
</Popover.Panel>
</Transition>

View File

@ -17,14 +17,13 @@ import { useProjectMyMembership } from "contexts/project-member.context";
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
// components
import { AllLists, AllBoards, FilterList } from "components/core";
import { AllLists, AllBoards, FilterList, CalendarView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
import { CalendarView } from "./calendar-view";
// icons
import {
ListBulletIcon,
@ -533,7 +532,13 @@ export const IssuesView: React.FC<Props> = ({
userAuth={memberRole}
/>
) : (
<CalendarView addIssueToDate={addIssueToDate} />
<CalendarView
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
addIssueToDate={addIssueToDate}
isCompleted={isCompleted}
userAuth={memberRole}
/>
)}
</>
) : type === "issue" ? (

View File

@ -84,7 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
(formData: Partial<IIssue>, issueId: string) => {
if (!workspaceSlug || !projectId) return;
if (cycleId)
@ -140,7 +140,7 @@ export const SingleListIssue: React.FC<Props> = ({
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
.then(() => {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
@ -151,18 +151,7 @@ export const SingleListIssue: React.FC<Props> = ({
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
});
},
[
workspaceSlug,
projectId,
cycleId,
moduleId,
issue,
groupTitle,
index,
selectedGroup,
orderBy,
params,
]
[workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
);
const handleCopyText = () => {

View File

@ -10,6 +10,7 @@ import { Dialog, Transition } from "@headlessui/react";
import issueServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
@ -18,14 +19,13 @@ import { SecondaryButton, DangerButton } from "components/ui";
import type { CycleIssueResponse, IIssue, ModuleIssueResponse } from "types";
// fetch-keys
import {
CYCLE_ISSUES,
CYCLE_CALENDAR_ISSUES,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES,
MODULE_CALENDAR_ISSUES,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_CALENDAR_ISSUES,
PROJECT_ISSUES_LIST_WITH_PARAMS,
USER_ISSUE,
} from "constants/fetch-keys";
import useIssuesView from "hooks/use-issues-view";
type Props = {
isOpen: boolean;
@ -39,7 +39,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { params } = useIssuesView();
const { issueView, params } = useIssuesView();
const { setToastAlert } = useToast();
@ -59,9 +59,19 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
await issueServices
.deleteIssue(workspaceSlug as string, projectId as string, data.id)
.then(() => {
if (issueView === "calendar") {
const calendarFetchKey = cycleId
? CYCLE_CALENDAR_ISSUES(projectId as string, cycleId as string)
: moduleId
? MODULE_CALENDAR_ISSUES(projectId as string, moduleId as string)
: PROJECT_CALENDAR_ISSUES(projectId as string);
mutate<IIssue[]>(calendarFetchKey);
} else {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
}
handleClose();
setToastAlert({

View File

@ -31,6 +31,9 @@ import {
MODULE_ISSUES_WITH_PARAMS,
CYCLE_DETAILS,
MODULE_DETAILS,
PROJECT_CALENDAR_ISSUES,
CYCLE_CALENDAR_ISSUES,
MODULE_CALENDAR_ISSUES,
} from "constants/fetch-keys";
export interface IssuesModalProps {
@ -55,7 +58,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { params } = useIssuesView();
const { issueView, params } = useIssuesView();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
@ -134,6 +137,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
const calendarFetchKey = cycleId
? CYCLE_CALENDAR_ISSUES(projectId as string, cycleId as string)
: moduleId
? MODULE_CALENDAR_ISSUES(projectId as string, moduleId as string)
: PROJECT_CALENDAR_ISSUES(projectId as string);
mutate<IIssue[]>(calendarFetchKey);
if (!createMore) handleClose();
setToastAlert({
@ -161,6 +172,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (issueView === "calendar") {
const calendarFetchKey = cycleId
? CYCLE_CALENDAR_ISSUES(projectId as string, cycleId as string)
: moduleId
? MODULE_CALENDAR_ISSUES(projectId as string, moduleId as string)
: PROJECT_CALENDAR_ISSUES(projectId as string);
mutate<IIssue[]>(calendarFetchKey, (prevData) =>
(prevData ?? []).map((i) => {
if (i.id === res.id) return { ...i, ...res };
return i;
})
);
} else {
mutate<IIssue[]>(
PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params),
@ -171,6 +196,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
})
);
}
}
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);

View File

@ -39,14 +39,14 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
(formData: Partial<IIssue>, issueId: string) => {
if (!workspaceSlug) return;
mutate<IIssue[]>(
USER_ISSUE(workspaceSlug as string),
(prevData) =>
prevData?.map((p) => {
if (p.id === issue.id) return { ...p, ...formData };
if (p.id === issueId) return { ...p, ...formData };
return p;
}),
@ -54,7 +54,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
.then((res) => {
mutate(USER_ISSUE(workspaceSlug as string));
})
@ -62,7 +62,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
console.log(error);
});
},
[workspaceSlug, projectId, issue]
[workspaceSlug, projectId]
);
const handleCopyText = () => {

View File

@ -18,7 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
@ -71,7 +71,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data });
partialUpdateIssue({ assignees_list: data }, issue.id);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{

View File

@ -11,7 +11,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
isNotAllowed: boolean;
};
@ -34,11 +34,14 @@ export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue,
placeholder="Due date"
value={issue?.target_date}
onChange={(val) => {
partialUpdateIssue({
partialUpdateIssue(
{
target_date: val,
priority: issue.priority,
state: issue.state,
});
},
issue.id
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,

View File

@ -15,7 +15,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
@ -41,7 +41,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
<CustomSelect
value={issue.estimate_point}
onChange={(val: number) => {
partialUpdateIssue({ estimate_point: val });
partialUpdateIssue({ estimate_point: val }, issue.id);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,

View File

@ -15,7 +15,7 @@ import trackEventServices from "services/track-event.service";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
@ -35,7 +35,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
<CustomSelect
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
partialUpdateIssue({ priority: data }, issue.id);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,

View File

@ -19,7 +19,7 @@ import { STATES_LIST } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
@ -60,11 +60,14 @@ export const ViewStateSelect: React.FC<Props> = ({
<CustomSearchSelect
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({
partialUpdateIssue(
{
state: data,
priority: issue.priority,
target_date: issue.target_date,
});
},
issue.id
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,

View File

@ -0,0 +1,4 @@
export interface ICalendarRange {
startDate: Date;
endDate: Date;
}

View File

@ -10,8 +10,9 @@ export * from "./views";
export * from "./integration";
export * from "./pages";
export * from "./ai";
export * from "./estimate"
export * from "./estimate";
export * from "./importer";
export * from "./calendar";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object