"use client"; import React, { SyntheticEvent, useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useParams, usePathname, useSearchParams } from "next/navigation"; import { Info, SquareUser } from "lucide-react"; // ui import { IModule } from "@plane/types"; import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui"; // components import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { ModuleQuickActions } from "@/components/modules"; import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; // constants import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common"; import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker"; import { MODULE_STATUS } from "@/constants/module"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useEventTracker, useMember, useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web constants import { EEstimateSystem } from "@/plane-web/constants/estimates"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { moduleId: string; }; export const ModuleCardItem: React.FC = observer((props) => { const { moduleId } = props; // refs const parentRef = useRef(null); // router const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); const searchParams = useSearchParams(); const pathname = usePathname(); // store hooks const { allowPermissions } = useUserPermissions(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); const { getUserDetails } = useMember(); const { captureEvent } = useEventTracker(); const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates(); // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT ); const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at; const renderIcon = Boolean(moduleDetails?.start_date) || Boolean(moduleDetails?.target_date); const { isMobile } = usePlatformOS(); const handleAddToFavorites = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); if (!workspaceSlug || !projectId) return; const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); } ); setPromiseToast(addToFavoritePromise, { loading: "Adding module to favorites...", success: { title: "Success!", message: () => "Module added to favorites.", }, error: { title: "Error!", message: () => "Couldn't add the module to favorites. Please try again.", }, }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); if (!workspaceSlug || !projectId) return; const removeFromFavoritePromise = removeModuleFromFavorites( workspaceSlug.toString(), projectId.toString(), moduleId ).then(() => { captureEvent(MODULE_UNFAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); }); setPromiseToast(removeFromFavoritePromise, { loading: "Removing module from favorites...", success: { title: "Success!", message: () => "Module removed from favorites.", }, error: { title: "Error!", message: () => "Couldn't remove the module from favorites. Please try again.", }, }); }; const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); e.preventDefault(); }; const handleModuleDetailsChange = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, payload) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); }) .catch((err) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: err?.detail ?? "Module could not be updated. Please try again.", }); }); }; const openModuleOverview = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); const query = generateQueryParams(searchParams, ["peekModule"]); if (searchParams.has("peekModule") && searchParams.get("peekModule") === moduleId) { router.push(`${pathname}?${query}`); } else { router.push(`${pathname}?${query && `${query}&`}peekModule=${moduleId}`); } }; if (!moduleDetails) return null; /** * NOTE: This completion percentage calculation is based on the total issues count. * when estimates are available and estimate type is points, we should consider the estimate point count * when estimates are available and estimate type is not points, then by default we consider the issue count */ const isEstimateEnabled = projectId && currentActiveEstimateId && areEstimateEnabledByProjectId(projectId.toString()) && estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS; const moduleTotalIssues = isEstimateEnabled ? moduleDetails?.total_estimate_points || 0 : moduleDetails.backlog_issues + moduleDetails.unstarted_issues + moduleDetails.started_issues + moduleDetails.completed_issues + moduleDetails.cancelled_issues; const moduleCompletedIssues = isEstimateEnabled ? moduleDetails?.completed_estimate_points || 0 : moduleDetails.completed_issues; // const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); const issueCount = module ? !moduleTotalIssues || moduleTotalIssues === 0 ? `0 ${isEstimateEnabled ? `Point` : `Issue`}` : moduleTotalIssues === moduleCompletedIssues ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? `s` : ``}` : `${moduleCompletedIssues}/${moduleTotalIssues} ${isEstimateEnabled ? `Points` : `Issues`}` : `0 ${isEstimateEnabled ? `Point` : `Issue`}`; const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined; const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({ id: index, name: group.title, value: moduleTotalIssues > 0 ? (moduleDetails[group.key as keyof IModule] as number) : 0, color: group.color, })); return (
{moduleDetails.name}
{moduleStatus && ( )}
{issueCount ?? "0 Issue"}
{moduleLeadDetails ? ( ) : ( )}
{ handleModuleDetailsChange({ start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null), target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null) }) }} placeholder={{ from: "Start date", to: "End date", }} disabled={isDisabled} hideIcon={{ from: renderIcon ?? true, to: renderIcon }} />
{isEditingAllowed && ( { if (moduleDetails.is_favorite) handleRemoveFromFavorites(e); else handleAddToFavorites(e); }} selected={!!moduleDetails.is_favorite} /> )} {workspaceSlug && projectId && ( )}
); });