import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; import isEmpty from "lodash/isEmpty"; // services import { CycleService } from "services/cycle.service"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; // ui import { CustomRangeDatePicker } from "components/ui"; import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; // icons import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarCheck2, CalendarClock, } from "lucide-react"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { findHowManyDaysLeft, isDateGreaterThanToday, renderFormattedPayloadDate, renderFormattedDate, } from "helpers/date-time.helper"; // types import { ICycle } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; import { CYCLE_UPDATED } from "constants/event-tracker"; // fetch-keys import { CYCLE_STATUS } from "constants/cycle"; type Props = { cycleId: string; handleClose: () => void; }; const defaultValues: Partial = { start_date: null, end_date: null, }; // services const cycleService = new CycleService(); // TODO: refactor the whole component export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycleId, handleClose } = props; // states const [cycleDeleteModal, setCycleDeleteModal] = useState(false); // refs const startDateButtonRef = useRef(null); const endDateButtonRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; // store hooks const { setTrackElement, captureCycleEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); const { getCycleById, updateCycleDetails } = useCycle(); const { getUserDetails } = useMember(); const cycleDetails = getCycleById(cycleId); const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; const { setToastAlert } = useToast(); const { setValue, reset, watch } = useForm({ defaultValues, }); const submitChanges = (data: Partial, changedProperty: string) => { if (!workspaceSlug || !projectId || !cycleId) return; updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data) .then((res) => { captureCycleEvent({ eventName: CYCLE_UPDATED, payload: { ...res, changed_properties: [changedProperty], element: "Right side-peek", state: "SUCCESS", }, }); }) .catch(() => { captureCycleEvent({ eventName: CYCLE_UPDATED, payload: { ...data, element: "Right side-peek", state: "FAILED", }, }); }); }; const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`) .then(() => { setToastAlert({ type: "success", title: "Link Copied!", message: "Cycle link copied to clipboard.", }); }) .catch(() => { setToastAlert({ type: "error", title: "Some error occurred", }); }); }; useEffect(() => { if (cycleDetails) reset({ ...cycleDetails, }); }, [cycleDetails, reset]); const dateChecker = async (payload: any) => { try { const res = await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload); return res.status; } catch (err) { return false; } }; const handleStartDateChange = async (date: string) => { setValue("start_date", date); if (!watch("end_date") || watch("end_date") === "") endDateButtonRef.current?.click(); if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") { if (!isDateGreaterThanToday(`${watch("end_date")}`)) { setToastAlert({ type: "error", title: "Error!", message: "Unable to create cycle in past date. Please enter a valid date.", }); reset({ ...cycleDetails }); return; } if (cycleDetails?.start_date && cycleDetails?.end_date) { const isDateValidForExistingCycle = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, cycle_id: cycleDetails.id, }); if (isDateValidForExistingCycle) { submitChanges( { start_date: renderFormattedPayloadDate(`${watch("start_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`), }, "start_date" ); setToastAlert({ type: "success", title: "Success!", message: "Cycle updated successfully.", }); } else { setToastAlert({ type: "error", title: "Error!", message: "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", }); } reset({ ...cycleDetails }); return; } const isDateValid = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, }); if (isDateValid) { submitChanges( { start_date: renderFormattedPayloadDate(`${watch("start_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`), }, "start_date" ); setToastAlert({ type: "success", title: "Success!", message: "Cycle updated successfully.", }); } else { setToastAlert({ type: "error", title: "Error!", message: "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", }); reset({ ...cycleDetails }); } } }; const handleEndDateChange = async (date: string) => { setValue("end_date", date); if (!watch("start_date") || watch("start_date") === "") startDateButtonRef.current?.click(); if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") { if (!isDateGreaterThanToday(`${watch("end_date")}`)) { setToastAlert({ type: "error", title: "Error!", message: "Unable to create cycle in past date. Please enter a valid date.", }); reset({ ...cycleDetails }); return; } if (cycleDetails?.start_date && cycleDetails?.end_date) { const isDateValidForExistingCycle = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, cycle_id: cycleDetails.id, }); if (isDateValidForExistingCycle) { submitChanges( { start_date: renderFormattedPayloadDate(`${watch("start_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`), }, "end_date" ); setToastAlert({ type: "success", title: "Success!", message: "Cycle updated successfully.", }); } else { setToastAlert({ type: "error", title: "Error!", message: "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", }); } reset({ ...cycleDetails }); return; } const isDateValid = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, }); if (isDateValid) { submitChanges( { start_date: renderFormattedPayloadDate(`${watch("start_date")}`), end_date: renderFormattedPayloadDate(`${watch("end_date")}`), }, "end_date" ); setToastAlert({ type: "success", title: "Success!", message: "Cycle updated successfully.", }); } else { setToastAlert({ type: "error", title: "Error!", message: "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", }); reset({ ...cycleDetails }); } } }; // TODO: refactor this // const handleFiltersUpdate = useCallback( // (key: keyof IIssueFilterOptions, value: string | string[]) => { // if (!workspaceSlug || !projectId) return; // const newValues = issueFilters?.filters?.[key] ?? []; // if (Array.isArray(value)) { // value.forEach((val) => { // if (!newValues.includes(val)) newValues.push(val); // }); // } else { // if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); // else newValues.push(value); // } // updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); // }, // [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] // ); const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date(); const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`); const progressPercentage = cycleDetails ? isCompleted && cycleDetails?.progress_snapshot ? Math.round( (cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100 ) : Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100) : null; if (!cycleDetails) return (
); const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? ""); const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? ""); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const issueCount = isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? cycleDetails.progress_snapshot.total_issues === 0 ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; return ( <> {cycleDetails && workspaceSlug && projectId && ( setCycleDeleteModal(false)} workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> )} <>
{!isCompleted && isEditingAllowed && ( { setTrackElement("CYCLE_PAGE_SIDEBAR"); setCycleDeleteModal(true); }} > Delete cycle )}
{currentCycle && ( {currentCycle.value === "current" && daysLeft !== undefined ? `${daysLeft} ${currentCycle.label}` : `${currentCycle.label}`} )}

{cycleDetails.name}

{cycleDetails.description && ( {cycleDetails.description} )}
Start date
{({ close }) => ( <> {renderFormattedDate(startDate) ?? "No date selected"} { if (val) { setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON"); handleStartDateChange(val); close(); } }} startDate={watch("start_date") ?? watch("end_date") ?? null} endDate={watch("end_date") ?? watch("start_date") ?? null} maxDate={new Date(`${watch("end_date")}`)} selectsStart={watch("end_date") ? true : false} /> )}
Target date
{({ close }) => ( <> {renderFormattedDate(endDate) ?? "No date selected"} { if (val) { setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON"); handleEndDateChange(val); close(); } }} startDate={watch("start_date") ?? watch("end_date") ?? null} endDate={watch("end_date") ?? watch("start_date") ?? null} minDate={new Date(`${watch("start_date")}`)} selectsEnd={watch("start_date") ? true : false} /> )}
Lead
{cycleOwnerDetails?.display_name}
Issues
{issueCount}
{({ open }) => (
Progress
{progressPercentage ? ( {progressPercentage ? `${progressPercentage}%` : ""} ) : ( "" )} {isStartValid && isEndValid ? (
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? ( <> {cycleDetails.progress_snapshot.distribution?.completion_chart && cycleDetails.start_date && cycleDetails.end_date && (
Ideal
Current
)} ) : ( <> {cycleDetails.distribution?.completion_chart && cycleDetails.start_date && cycleDetails.end_date && (
Ideal
Current
)} )} {/* stats */} {isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? ( <> {cycleDetails.progress_snapshot.total_issues > 0 && cycleDetails.progress_snapshot.distribution && (
)} ) : ( <> {cycleDetails.total_issues > 0 && cycleDetails.distribution && (
)} )}
)}
); });