import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; import { AlertCircle, CalendarCheck2, CalendarClock, ChevronDown, ChevronRight, Info, LinkIcon, Plus, Trash2, UserCircle2, } from "lucide-react"; // hooks import { useModule, useUser, useEventTracker } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; import { ProjectMemberDropdown } from "components/dropdowns"; // ui import { CustomRangeDatePicker } from "components/ui"; import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui"; // helpers import { isDateGreaterThanToday, renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // constant import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead: "", members: [], start_date: null, target_date: null, status: "backlog", }; type Props = { moduleId: string; handleClose: () => void; }; // TODO: refactor this component export const ModuleDetailsSidebar: React.FC = observer((props) => { const { moduleId, handleClose } = props; // states const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); // refs const startDateButtonRef = useRef(null); const endDateButtonRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; // store hooks const { membership: { currentProjectRole }, } = useUser(); const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule(); const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); const { setToastAlert } = useToast(); const { setValue, watch, reset, control } = useForm({ defaultValues, }); const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !moduleId) return; updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data) .then((res) => { captureModuleEvent({ eventName: MODULE_UPDATED, payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" }, }); }) .catch((_) => { captureModuleEvent({ eventName: MODULE_UPDATED, payload: { ...data, state: "FAILED" }, }); }); }; const handleCreateLink = async (formData: ModuleLink) => { if (!workspaceSlug || !projectId || !moduleId) return; const payload = { metadata: {}, ...formData }; createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload) .then(() => { captureEvent(MODULE_LINK_CREATED, { module_id: moduleId, state: "SUCCESS", }); setToastAlert({ type: "success", title: "Module link created", message: "Module link created successfully.", }); }) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Some error occurred", }); }); }; const handleUpdateLink = async (formData: ModuleLink, linkId: string) => { if (!workspaceSlug || !projectId || !module) return; const payload = { metadata: {}, ...formData }; updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload) .then(() => { captureEvent(MODULE_LINK_UPDATED, { module_id: moduleId, state: "SUCCESS", }); setToastAlert({ type: "success", title: "Module link updated", message: "Module link updated successfully.", }); }) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Some error occurred", }); }); }; const handleDeleteLink = async (linkId: string) => { if (!workspaceSlug || !projectId || !module) return; deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId) .then(() => { captureEvent(MODULE_LINK_DELETED, { module_id: moduleId, state: "SUCCESS", }); setToastAlert({ type: "success", title: "Module link deleted", message: "Module link deleted successfully.", }); }) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Some error occurred", }); }); }; const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) .then(() => { setToastAlert({ type: "success", title: "Link copied", message: "Module link copied to clipboard", }); }) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Some error occurred", }); }); }; const handleStartDateChange = async (date: string) => { setValue("start_date", date); if (!watch("target_date") || watch("target_date") === "") endDateButtonRef.current?.click(); if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { submitChanges({ start_date: renderFormattedPayloadDate(`${watch("start_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`), }); setToastAlert({ type: "success", title: "Success!", message: "Module updated successfully.", }); } }; const handleEndDateChange = async (date: string) => { setValue("target_date", date); if (!watch("start_date") || watch("start_date") === "") endDateButtonRef.current?.click(); if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { submitChanges({ target_date: renderFormattedPayloadDate(`${watch("target_date")}`), start_date: renderFormattedPayloadDate(`${watch("start_date")}`), }); setToastAlert({ type: "success", title: "Success!", message: "Module updated successfully.", }); } }; useEffect(() => { if (moduleDetails) reset({ ...moduleDetails, }); }, [moduleDetails, reset]); const isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date(); const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`); const progressPercentage = moduleDetails ? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100) : null; const handleEditLink = (link: ILinkDetails) => { setSelectedLinkToUpdate(link); setModuleLinkModal(true); }; if (!moduleDetails) return (
); const startDate = new Date(watch("start_date") ?? moduleDetails.start_date ?? ""); const endDate = new Date(watch("target_date") ?? moduleDetails.target_date ?? ""); const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); const issueCount = moduleDetails.total_issues === 0 ? "0 Issue" : `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( <> { setModuleLinkModal(false); setSelectedLinkToUpdate(null); }} data={selectedLinkToUpdate} status={selectedLinkToUpdate ? true : false} createIssueLink={handleCreateLink} updateIssueLink={handleUpdateLink} /> setModuleDeleteModal(false)} data={moduleDetails} /> <>
{isEditingAllowed && ( { setTrackElement("Module peek-overview"); setModuleDeleteModal(true); }} > Delete module )}
( {moduleStatus?.label ?? "Backlog"} } value={value} onChange={(value: any) => { submitChanges({ status: value }); }} disabled={!isEditingAllowed} > {MODULE_STATUS.map((status) => (
{status.label}
))}
)} />

{moduleDetails.name}

{moduleDetails.description && ( {moduleDetails.description} )}
Start date
{({ close }) => ( <> {renderFormattedDate(startDate) ?? "No date selected"} { if (val) { handleStartDateChange(val); close(); } }} startDate={watch("start_date") ?? watch("target_date") ?? null} endDate={watch("target_date") ?? watch("start_date") ?? null} maxDate={new Date(`${watch("target_date")}`)} selectsStart={watch("target_date") ? true : false} /> )}
Target date
{({ close }) => ( <> {renderFormattedDate(endDate) ?? "No date selected"} { if (val) { handleEndDateChange(val); close(); } }} startDate={watch("start_date") ?? watch("target_date") ?? null} endDate={watch("target_date") ?? watch("start_date") ?? null} minDate={new Date(`${watch("start_date")}`)} selectsEnd={watch("start_date") ? true : false} /> )}
{moduleDetails.description && ( {moduleDetails.description} )}
Lead
(
{ submitChanges({ lead: val }); }} projectId={projectId?.toString() ?? ""} multiple={false} buttonVariant="background-with-text" placeholder="Lead" />
)} />
Members
(
{ submitChanges({ members: val }); }} multiple projectId={projectId?.toString() ?? ""} buttonVariant={value && value?.length > 0 ? "transparent-without-text" : "background-with-text"} buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""} disabled={!isEditingAllowed} />
)} />
Issues
{issueCount}
{({ open }) => (
Progress
{progressPercentage ? ( {progressPercentage ? `${progressPercentage}%` : ""} ) : ( "" )} {isStartValid && isEndValid ? (
{moduleDetails.start_date && moduleDetails.target_date ? (
Ideal
Current
) : ( "" )} {moduleDetails.total_issues > 0 && (
)}
)}
{({ open }) => (
Links
{currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( <> {isEditingAllowed && (
)} ) : (
No links added yet
)}
)}
); });