From fe280b2bebedf0bea70e18db6db14df8a670a770 Mon Sep 17 00:00:00 2001 From: Ketan Sharma Date: Mon, 9 Sep 2024 16:50:56 +0530 Subject: [PATCH] [WEB-2106] fix: add date and state change functionalities to list and grid view (#5533) * added functionality to list and grid * fixed logic for archived module * fixed logic for list view * improved logic and fixed linting issues * improved variable names --- .../components/modules/module-card-item.tsx | 99 ++++++++++++------- .../modules/module-list-item-action.tsx | 84 ++++++++++------ .../modules/module-status-dropdown.tsx | 50 ++++++++++ 3 files changed, 171 insertions(+), 62 deletions(-) create mode 100644 web/core/components/modules/module-status-dropdown.tsx diff --git a/web/core/components/modules/module-card-item.tsx b/web/core/components/modules/module-card-item.tsx index 031941553a..d71bd52ed6 100644 --- a/web/core/components/modules/module-card-item.tsx +++ b/web/core/components/modules/module-card-item.tsx @@ -1,23 +1,25 @@ "use client"; -import React, { useRef } from "react"; +import React, { SyntheticEvent, useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useParams, usePathname, useSearchParams } from "next/navigation"; -import { CalendarCheck2, CalendarClock, Info, MoveRight, SquareUser } from "lucide-react"; +import { Info, SquareUser } from "lucide-react"; // ui import { IModule } from "@plane/types"; -import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, Tooltip, setPromiseToast } from "@plane/ui"; +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"; import { EUserProjectRoles } from "@/constants/project"; // helpers -import { getDate, renderFormattedDate } from "@/helpers/date-time.helper"; +import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useEventTracker, useMember, useModule, useProjectEstimates, useUser } from "@/hooks/store"; @@ -43,7 +45,7 @@ export const ModuleCardItem: React.FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); - const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule(); + const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); const { getUserDetails } = useMember(); const { captureEvent } = useEventTracker(); const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates(); @@ -51,6 +53,10 @@ export const ModuleCardItem: React.FC = observer((props) => { // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + 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(); @@ -110,6 +116,31 @@ export const ModuleCardItem: React.FC = observer((props) => { }); }; + 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(); @@ -147,10 +178,6 @@ export const ModuleCardItem: React.FC = observer((props) => { ? moduleDetails?.completed_estimate_points || 0 : moduleDetails.completed_issues; - const endDate = getDate(moduleDetails.target_date); - const startDate = getDate(moduleDetails.start_date); - - const isDateValid = moduleDetails.target_date || moduleDetails.start_date; // const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); @@ -174,7 +201,7 @@ export const ModuleCardItem: React.FC = observer((props) => { })); return ( -
+
@@ -182,17 +209,13 @@ export const ModuleCardItem: React.FC = observer((props) => { {moduleDetails.name} -
+
{moduleStatus && ( - - {moduleStatus.label} - + )}
-
- {isDateValid ? ( -
- - {renderFormattedDate(startDate)} - - - {renderFormattedDate(endDate)} -
- ) : ( - No due date - )} +
+ { + 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 }} + />
@@ -254,4 +287,4 @@ export const ModuleCardItem: React.FC = observer((props) => {
); -}); +}); \ No newline at end of file diff --git a/web/core/components/modules/module-list-item-action.tsx b/web/core/components/modules/module-list-item-action.tsx index b1df2d3598..fcba7363be 100644 --- a/web/core/components/modules/module-list-item-action.tsx +++ b/web/core/components/modules/module-list-item-action.tsx @@ -4,20 +4,21 @@ import React, { FC } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { CalendarCheck2, CalendarClock, MoveRight, SquareUser } from "lucide-react"; +import { SquareUser } from "lucide-react"; // types import { IModule } from "@plane/types"; // ui -import { FavoriteStar, Tooltip, setPromiseToast } from "@plane/ui"; +import { FavoriteStar, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui"; // components +import { DateRangeDropdown } from "@/components/dropdowns"; import { ModuleQuickActions } from "@/components/modules"; +import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; // constants import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker"; import { MODULE_STATUS } from "@/constants/module"; import { EUserProjectRoles } from "@/constants/project"; -// helpers -import { getDate, renderFormattedDate } from "@/helpers/date-time.helper"; // hooks +import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store"; import { ButtonAvatars } from "../dropdowns/member/avatar"; @@ -35,19 +36,16 @@ export const ModuleListItemAction: FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); - const { addModuleToFavorites, removeModuleFromFavorites } = useModule(); + const { addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); const { getUserDetails } = useMember(); const { captureEvent } = useEventTracker(); // derived values - const endDate = getDate(moduleDetails.target_date); - const startDate = getDate(moduleDetails.start_date); - - const renderDate = moduleDetails.start_date || moduleDetails.target_date; const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at; + const renderIcon = Boolean(moduleDetails.start_date) || Boolean(moduleDetails.target_date); // handlers const handleAddToFavorites = (e: React.MouseEvent) => { @@ -108,30 +106,58 @@ export const ModuleListItemAction: FC = observer((props) => { }); }; + 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 moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined; return ( <> - {renderDate && ( -
- - {renderFormattedDate(startDate)} - - - {renderFormattedDate(endDate)} -
- )} + { + 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 }} + /> {moduleStatus && ( - - {moduleStatus.label} - + )} {moduleLeadDetails ? ( @@ -165,4 +191,4 @@ export const ModuleListItemAction: FC = observer((props) => { )} ); -}); +}); \ No newline at end of file diff --git a/web/core/components/modules/module-status-dropdown.tsx b/web/core/components/modules/module-status-dropdown.tsx new file mode 100644 index 0000000000..ec021c49f9 --- /dev/null +++ b/web/core/components/modules/module-status-dropdown.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react' +import { observer } from 'mobx-react'; +import { IModule } from '@plane/types'; +import { CustomSelect, TModuleStatus, ModuleStatusIcon } from '@plane/ui' +import { MODULE_STATUS } from '@/constants/module' + +type Props = { + isDisabled: boolean; + moduleDetails: IModule; + handleModuleDetailsChange: (payload: Partial) => Promise; +}; + +export const ModuleStatusDropdown : FC = observer((props : Props) => { + const {isDisabled, moduleDetails, handleModuleDetailsChange} = props; + const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); + + if(!moduleStatus) return <> + +return ( + + {moduleStatus?.label ?? "Backlog"} + + } + value={moduleStatus?.value} + onChange={(val: TModuleStatus)=>{ + handleModuleDetailsChange({status: val}) + }} + disabled={isDisabled} + > + {MODULE_STATUS.map((status) => ( + +
+ + {status.label} +
+
+ ))} +
+ ) +}) \ No newline at end of file