import React, { useCallback, useEffect, 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"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; // ui import { CustomRangeDatePicker } from "components/ui"; import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon } from "@plane/ui"; // icon import { AlertCircle, CalendarCheck2, CalendarClock, ChevronDown, ChevronRight, Info, LinkIcon, Plus, Trash2, } from "lucide-react"; // helpers import { isDateGreaterThanToday, renderDateFormat, renderShortDate, renderShortMonthDate, } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IIssueFilterOptions, ILinkDetails, IModule, ModuleLink } from "types"; import { EFilterType } from "store/issues/types"; // constant import { MODULE_STATUS } from "constants/module"; import { EUserWorkspaceRoles } from "constants/workspace"; 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; const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; const { module: { moduleDetails: _moduleDetails, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, }, moduleIssuesFilter: { issueFilters, updateFilters }, user: userStore, } = useMobxStore(); const userRole = userStore.currentProjectRole; const moduleDetails = _moduleDetails[moduleId] ?? undefined; 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, data); }; const handleCreateLink = async (formData: ModuleLink) => { if (!workspaceSlug || !projectId || !moduleId) return; const payload = { metadata: {}, ...formData }; createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload) .then(() => { 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(() => { 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(() => { 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("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { if (!isDateGreaterThanToday(`${watch("target_date")}`)) { setToastAlert({ type: "error", title: "Error!", message: "Unable to create module in past date. Please enter a valid date.", }); return; } submitChanges({ start_date: renderDateFormat(`${watch("start_date")}`), target_date: renderDateFormat(`${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("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { if (!isDateGreaterThanToday(`${watch("target_date")}`)) { setToastAlert({ type: "error", title: "Error!", message: "Unable to create module in past date. Please enter a valid date.", }); return; } submitChanges({ start_date: renderDateFormat(`${watch("start_date")}`), target_date: renderDateFormat(`${watch("target_date")}`), }); setToastAlert({ type: "success", title: "Success!", message: "Module updated successfully.", }); } }; 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 }, moduleId ); }, [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] ); 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 areYearsEqual = startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear()); const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); const issueCount = moduleDetails.total_issues === 0 ? "0 Issue" : moduleDetails.total_issues === moduleDetails.completed_issues ? moduleDetails.total_issues > 1 ? `${moduleDetails.total_issues}` : `${moduleDetails.total_issues}` : `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`; const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; return ( <> { setModuleLinkModal(false); setSelectedLinkToUpdate(null); }} data={selectedLinkToUpdate} status={selectedLinkToUpdate ? true : false} createIssueLink={handleCreateLink} updateIssueLink={handleUpdateLink} /> setModuleDeleteModal(false)} data={moduleDetails} /> <>
{isEditingAllowed && ( 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
{areYearsEqual ? renderShortDate(startDate, "No date selected") : renderShortMonthDate(startDate, "No date selected")} { if (val) { handleStartDateChange(val); } }} 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
<> {areYearsEqual ? renderShortDate(endDate, "No date selected") : renderShortMonthDate(endDate, "No date selected")} { if (val) { handleEndDateChange(val); } }} 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} />
( { submitChanges({ lead: val }); }} /> )} /> ( { submitChanges({ members: val }); }} /> )} />
Issues
{issueCount}
{({ open }) => (
Progress
{progressPercentage ? ( {progressPercentage ? `${progressPercentage}%` : ""} ) : ( "" )} {isStartValid && isEndValid ? (
{isStartValid && isEndValid ? (
Ideal
Current
) : ( "" )} {moduleDetails.total_issues > 0 && (
)}
)}
{({ open }) => (
Links
{userRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( <> {isEditingAllowed && (
)} ) : (
No links added yet
{isEditingAllowed && ( )}
)}
)}
); });