forked from github/plane
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:
parent
4f2b106852
commit
c7deb00f2a
@ -98,7 +98,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (cycleId)
|
if (cycleId)
|
||||||
@ -164,7 +164,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (cycleId) {
|
if (cycleId) {
|
||||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
||||||
@ -178,18 +178,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
cycleId,
|
|
||||||
moduleId,
|
|
||||||
issue,
|
|
||||||
groupTitle,
|
|
||||||
index,
|
|
||||||
selectedGroup,
|
|
||||||
orderBy,
|
|
||||||
params,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStyle = (
|
const getStyle = (
|
||||||
|
217
apps/app/components/core/calendar-view/calendar-header.tsx
Normal file
217
apps/app/components/core/calendar-view/calendar-header.tsx
Normal 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;
|
@ -1,8 +1,27 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// swr
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
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
|
// helper
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat } from "helpers/date-time.helper";
|
||||||
import {
|
import {
|
||||||
@ -11,62 +30,28 @@ import {
|
|||||||
eachDayOfInterval,
|
eachDayOfInterval,
|
||||||
weekDayInterval,
|
weekDayInterval,
|
||||||
formatDate,
|
formatDate,
|
||||||
getCurrentWeekStartDate,
|
|
||||||
getCurrentWeekEndDate,
|
|
||||||
subtractMonths,
|
|
||||||
addMonths,
|
|
||||||
updateDateWithYear,
|
|
||||||
updateDateWithMonth,
|
|
||||||
isSameMonth,
|
|
||||||
isSameYear,
|
|
||||||
subtract7DaysToDate,
|
|
||||||
addSevenDaysToDate,
|
|
||||||
} from "helpers/calendar.helper";
|
} 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
|
// type
|
||||||
import { IIssue } from "types";
|
import { ICalendarRange, IIssue, UserAuth } from "types";
|
||||||
// constant
|
|
||||||
import { monthOptions, yearOptions } from "constants/calendar";
|
|
||||||
import modulesService from "services/modules.service";
|
|
||||||
import { getStateGroupIcon } from "components/icons";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
addIssueToDate: (date: string) => void;
|
addIssueToDate: (date: string) => void;
|
||||||
|
isCompleted: boolean;
|
||||||
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ICalendarRange {
|
export const CalendarView: React.FC<Props> = ({
|
||||||
startDate: Date;
|
handleEditIssue,
|
||||||
endDate: Date;
|
handleDeleteIssue,
|
||||||
}
|
addIssueToDate,
|
||||||
|
isCompleted = false,
|
||||||
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
userAuth,
|
||||||
|
}) => {
|
||||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
||||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -78,12 +63,6 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
|||||||
endDate: lastDayOfWeek(currentDate),
|
endDate: lastDayOfWeek(currentDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetDateFilter = {
|
|
||||||
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
|
||||||
calendarDateRange.endDate
|
|
||||||
)};before`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: projectCalendarIssues } = useSWR(
|
const { data: projectCalendarIssues } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -218,304 +197,73 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDate = (date: Date) => {
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||||
setCurrentDate(date);
|
|
||||||
|
|
||||||
setCalendarDateRange({
|
|
||||||
startDate: startOfWeek(date),
|
|
||||||
endDate: lastDayOfWeek(date),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return calendarIssues ? (
|
return calendarIssues ? (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<div className="mb-4 h-full">
|
||||||
<div className="-m-2 h-full overflow-y-auto rounded-lg p-8 text-brand-secondary">
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="-m-2 h-full rounded-lg p-8 text-brand-secondary">
|
||||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
<CalendarHeader
|
||||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
isMonthlyView={isMonthlyView}
|
||||||
{({ open }) => (
|
setIsMonthlyView={setIsMonthlyView}
|
||||||
<>
|
showWeekEnds={showWeekEnds}
|
||||||
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
|
setShowWeekEnds={setShowWeekEnds}
|
||||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
|
currentDate={currentDate}
|
||||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
setCurrentDate={setCurrentDate}
|
||||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
setCalendarDateRange={setCalendarDateRange}
|
||||||
</div>
|
/>
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
<div
|
||||||
as={React.Fragment}
|
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
|
||||||
enter="transition ease-out duration-200"
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
enterFrom="opacity-0 translate-y-1"
|
}`}
|
||||||
enterTo="opacity-100 translate-y-0"
|
>
|
||||||
leave="transition ease-in duration-150"
|
{weeks.map((date, index) => (
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
<div
|
||||||
leaveTo="opacity-0 translate-y-1"
|
key={index}
|
||||||
>
|
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
|
||||||
<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">
|
!isMonthlyView
|
||||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
? showWeekEnds
|
||||||
{yearOptions.map((year) => (
|
? (index + 1) % 7 === 0
|
||||||
<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>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
|
|
||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{weeks.map((date, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
|
|
||||||
!isMonthlyView
|
|
||||||
? showWeekEnds
|
|
||||||
? (index + 1) % 7 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: (index + 1) % 5 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{isMonthlyView ? formatDate(date, "eee").substring(0, 3) : formatDate(date, "eee")}
|
|
||||||
</span>
|
|
||||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`grid h-full auto-rows-[minmax(150px,1fr)] ${
|
|
||||||
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"
|
: "border-r"
|
||||||
}`}
|
: (index + 1) % 5 === 0
|
||||||
>
|
? ""
|
||||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
: "border-r"
|
||||||
{totalIssues > 0 &&
|
: ""
|
||||||
date.issues
|
}`}
|
||||||
.slice(0, showAllIssues ? totalIssues : 4)
|
>
|
||||||
.map((issue: IIssue, index) => (
|
<span>
|
||||||
<Draggable draggableId={issue.id} index={index}>
|
{isMonthlyView
|
||||||
{(provided, snapshot) => (
|
? formatDate(date, "eee").substring(0, 3)
|
||||||
<div
|
: formatDate(date, "eee")}
|
||||||
key={index}
|
</span>
|
||||||
ref={provided.innerRef}
|
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
||||||
{...provided.draggableProps}
|
</div>
|
||||||
{...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 ${
|
</div>
|
||||||
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
|
|
||||||
}`}
|
<div
|
||||||
>
|
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
||||||
<Link
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
|
} `}
|
||||||
>
|
>
|
||||||
<a className="flex w-full items-center gap-2">
|
{currentViewDaysData.map((date, index) => (
|
||||||
{getStateGroupIcon(
|
<SingleCalendarDate
|
||||||
issue.state_detail.group,
|
index={index}
|
||||||
"12",
|
date={date}
|
||||||
"12",
|
handleEditIssue={handleEditIssue}
|
||||||
issue.state_detail.color
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
)}
|
addIssueToDate={addIssueToDate}
|
||||||
{issue.name}
|
isMonthlyView={isMonthlyView}
|
||||||
</a>
|
showWeekEnds={showWeekEnds}
|
||||||
</Link>
|
isNotAllowed={isNotAllowed}
|
||||||
</div>
|
/>
|
||||||
)}
|
))}
|
||||||
</Draggable>
|
</div>
|
||||||
))}
|
|
||||||
{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>
|
||||||
</div>
|
</DragDropContext>
|
||||||
</DragDropContext>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -1 +1,4 @@
|
|||||||
export * from "./calendar"
|
export * from "./calendar";
|
||||||
|
export * from "./single-date";
|
||||||
|
export * from "./single-issue";
|
||||||
|
export * from "./calendar-header";
|
||||||
|
107
apps/app/components/core/calendar-view/single-date.tsx
Normal file
107
apps/app/components/core/calendar-view/single-date.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
262
apps/app/components/core/calendar-view/single-issue.tsx
Normal file
262
apps/app/components/core/calendar-view/single-issue.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./board-view";
|
export * from "./board-view";
|
||||||
|
export * from "./calendar-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
|
@ -233,31 +233,30 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
|
||||||
{Object.keys(properties).map((key) => {
|
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
|
||||||
|
|
||||||
return (
|
<div className="space-y-2 py-3">
|
||||||
<button
|
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
||||||
key={key}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
type="button"
|
{Object.keys(properties).map((key) => {
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
properties[key as keyof Properties]
|
|
||||||
? "border-brand-accent bg-brand-accent text-white"
|
return (
|
||||||
: "border-brand-base"
|
<button
|
||||||
}`}
|
key={key}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
type="button"
|
||||||
>
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
properties[key as keyof Properties]
|
||||||
</button>
|
? "border-brand-accent bg-brand-accent text-white"
|
||||||
);
|
: "border-brand-base"
|
||||||
})}
|
}`}
|
||||||
</div>
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
|
>
|
||||||
|
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -17,14 +17,13 @@ import { useProjectMyMembership } from "contexts/project-member.context";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { AllLists, AllBoards, FilterList } from "components/core";
|
import { AllLists, AllBoards, FilterList, CalendarView } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
|
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
|
||||||
import { CalendarView } from "./calendar-view";
|
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
@ -533,7 +532,13 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CalendarView addIssueToDate={addIssueToDate} />
|
<CalendarView
|
||||||
|
handleEditIssue={handleEditIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
addIssueToDate={addIssueToDate}
|
||||||
|
isCompleted={isCompleted}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : type === "issue" ? (
|
) : type === "issue" ? (
|
||||||
|
@ -84,7 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
|
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (cycleId)
|
if (cycleId)
|
||||||
@ -140,7 +140,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (cycleId) {
|
if (cycleId) {
|
||||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
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));
|
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
cycleId,
|
|
||||||
moduleId,
|
|
||||||
issue,
|
|
||||||
groupTitle,
|
|
||||||
index,
|
|
||||||
selectedGroup,
|
|
||||||
orderBy,
|
|
||||||
params,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
|
@ -10,6 +10,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
import issueServices from "services/issues.service";
|
import issueServices from "services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// icons
|
// icons
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// ui
|
// ui
|
||||||
@ -18,14 +19,13 @@ import { SecondaryButton, DangerButton } from "components/ui";
|
|||||||
import type { CycleIssueResponse, IIssue, ModuleIssueResponse } from "types";
|
import type { CycleIssueResponse, IIssue, ModuleIssueResponse } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
CYCLE_ISSUES,
|
CYCLE_CALENDAR_ISSUES,
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES,
|
MODULE_CALENDAR_ISSUES,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_CALENDAR_ISSUES,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
USER_ISSUE,
|
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -39,7 +39,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { params } = useIssuesView();
|
const { issueView, params } = useIssuesView();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -59,9 +59,19 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
|
|||||||
await issueServices
|
await issueServices
|
||||||
.deleteIssue(workspaceSlug as string, projectId as string, data.id)
|
.deleteIssue(workspaceSlug as string, projectId as string, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
if (issueView === "calendar") {
|
||||||
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
const calendarFetchKey = cycleId
|
||||||
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
? 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();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -31,6 +31,9 @@ import {
|
|||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
CYCLE_DETAILS,
|
CYCLE_DETAILS,
|
||||||
MODULE_DETAILS,
|
MODULE_DETAILS,
|
||||||
|
PROJECT_CALENDAR_ISSUES,
|
||||||
|
CYCLE_CALENDAR_ISSUES,
|
||||||
|
MODULE_CALENDAR_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
export interface IssuesModalProps {
|
export interface IssuesModalProps {
|
||||||
@ -55,7 +58,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { params } = useIssuesView();
|
const { issueView, params } = useIssuesView();
|
||||||
|
|
||||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId 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.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
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();
|
if (!createMore) handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -162,14 +173,29 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (isUpdatingSingleIssue) {
|
if (isUpdatingSingleIssue) {
|
||||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||||
} else {
|
} else {
|
||||||
mutate<IIssue[]>(
|
if (issueView === "calendar") {
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params),
|
const calendarFetchKey = cycleId
|
||||||
(prevData) =>
|
? 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) => {
|
(prevData ?? []).map((i) => {
|
||||||
if (i.id === res.id) return { ...i, ...res };
|
if (i.id === res.id) return { ...i, ...res };
|
||||||
return i;
|
return i;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
mutate<IIssue[]>(
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params),
|
||||||
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((i) => {
|
||||||
|
if (i.id === res.id) return { ...i, ...res };
|
||||||
|
return i;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||||
|
@ -39,14 +39,14 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>, issueId: string) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
mutate<IIssue[]>(
|
||||||
USER_ISSUE(workspaceSlug as string),
|
USER_ISSUE(workspaceSlug as string),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
prevData?.map((p) => {
|
prevData?.map((p) => {
|
||||||
if (p.id === issue.id) return { ...p, ...formData };
|
if (p.id === issueId) return { ...p, ...formData };
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}),
|
}),
|
||||||
@ -54,7 +54,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
|||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(USER_ISSUE(workspaceSlug as string));
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
})
|
})
|
||||||
@ -62,7 +62,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[workspaceSlug, projectId, issue]
|
[workspaceSlug, projectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
|
@ -18,7 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
tooltipPosition?: "left" | "right";
|
tooltipPosition?: "left" | "right";
|
||||||
@ -71,7 +71,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||||
else newData.push(data);
|
else newData.push(data);
|
||||||
|
|
||||||
partialUpdateIssue({ assignees_list: data });
|
partialUpdateIssue({ assignees_list: data }, issue.id);
|
||||||
|
|
||||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ import { IIssue } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,11 +34,14 @@ export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue,
|
|||||||
placeholder="Due date"
|
placeholder="Due date"
|
||||||
value={issue?.target_date}
|
value={issue?.target_date}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
partialUpdateIssue({
|
partialUpdateIssue(
|
||||||
target_date: val,
|
{
|
||||||
priority: issue.priority,
|
target_date: val,
|
||||||
state: issue.state,
|
priority: issue.priority,
|
||||||
});
|
state: issue.state,
|
||||||
|
},
|
||||||
|
issue.id
|
||||||
|
);
|
||||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
@ -15,7 +15,7 @@ import { IIssue } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -41,7 +41,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={issue.estimate_point}
|
value={issue.estimate_point}
|
||||||
onChange={(val: number) => {
|
onChange={(val: number) => {
|
||||||
partialUpdateIssue({ estimate_point: val });
|
partialUpdateIssue({ estimate_point: val }, issue.id);
|
||||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
@ -15,7 +15,7 @@ import trackEventServices from "services/track-event.service";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -35,7 +35,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={issue.priority}
|
value={issue.priority}
|
||||||
onChange={(data: string) => {
|
onChange={(data: string) => {
|
||||||
partialUpdateIssue({ priority: data });
|
partialUpdateIssue({ priority: data }, issue.id);
|
||||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
@ -19,7 +19,7 @@ import { STATES_LIST } from "constants/fetch-keys";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
@ -60,11 +60,14 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue.state}
|
value={issue.state}
|
||||||
onChange={(data: string) => {
|
onChange={(data: string) => {
|
||||||
partialUpdateIssue({
|
partialUpdateIssue(
|
||||||
state: data,
|
{
|
||||||
priority: issue.priority,
|
state: data,
|
||||||
target_date: issue.target_date,
|
priority: issue.priority,
|
||||||
});
|
target_date: issue.target_date,
|
||||||
|
},
|
||||||
|
issue.id
|
||||||
|
);
|
||||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
{
|
{
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
4
apps/app/types/calendar.ts
Normal file
4
apps/app/types/calendar.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface ICalendarRange {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
}
|
3
apps/app/types/index.d.ts
vendored
3
apps/app/types/index.d.ts
vendored
@ -10,8 +10,9 @@ export * from "./views";
|
|||||||
export * from "./integration";
|
export * from "./integration";
|
||||||
export * from "./pages";
|
export * from "./pages";
|
||||||
export * from "./ai";
|
export * from "./ai";
|
||||||
export * from "./estimate"
|
export * from "./estimate";
|
||||||
export * from "./importer";
|
export * from "./importer";
|
||||||
|
export * from "./calendar";
|
||||||
|
|
||||||
export type NestedKeyOf<ObjectType extends object> = {
|
export type NestedKeyOf<ObjectType extends object> = {
|
||||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||||
|
Loading…
Reference in New Issue
Block a user