import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { AlertCircle, CalendarClock, ChevronDown, ChevronRight, Info, LinkIcon, Plus, Trash2, UserCircle2, } from "lucide-react"; // ui import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon, TOAST_TYPE, setToast, } from "@plane/ui"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; import { DeleteModuleModal } from "components/modules"; // constant import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; // helpers import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // hooks import { useModule, useUser, useEventTracker } from "hooks/store"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; const defaultValues: Partial = { lead_id: "", member_ids: [], 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); // 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 { 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", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Module link created", message: "Module link created successfully.", }); }) .catch(() => { setToast({ type: TOAST_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", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Module link updated", message: "Module link updated successfully.", }); }) .catch(() => { setToast({ type: TOAST_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", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Module link deleted", message: "Module link deleted successfully.", }); }) .catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); }); }; const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Module link copied to clipboard", }); }) .catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); }); }; const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => { submitChanges({ start_date: startDate ? renderFormattedPayloadDate(startDate) : null, target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null, }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); }; useEffect(() => { if (moduleDetails) reset({ ...moduleDetails, }); }, [moduleDetails, reset]); const startDate = getDate(moduleDetails?.start_date); const endDate = getDate(moduleDetails?.target_date); const isStartValid = startDate && startDate <= new Date(); const isEndValid = startDate && endDate && endDate >= startDate; 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 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} )}
Date range
( { const startDate = getDate(startDateValue); const endDate = getDate(endDateValue); return ( { onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); handleDateChange(val?.from, val?.to); }} placeholder={{ from: "Start date", to: "Target date", }} /> ); }} /> )} />
Lead
(
{ submitChanges({ lead_id: val }); }} projectId={projectId?.toString() ?? ""} multiple={false} buttonVariant="background-with-text" placeholder="Lead" />
)} />
Members
(
{ submitChanges({ member_ids: 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
)}
)}
); });