From f362afb001176c4e75df6f8bdf3a6fd2013d023b Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 28 May 2024 15:06:22 +0530 Subject: [PATCH] chore: handle estimate update worklfow --- apiserver/plane/app/views/estimate/base.py | 3 +- web/components/estimates/create/modal.tsx | 5 +- web/components/estimates/create/stage-one.tsx | 7 +- web/components/estimates/points/delete.tsx | 100 ---------------- web/components/estimates/points/edit-root.tsx | 29 ----- .../estimates/points/{ => edit}/create.tsx | 69 +++++++---- .../estimates/points/edit/delete.tsx | 111 ++++++++++++++++++ web/components/estimates/points/edit/index.ts | 5 + .../estimates/points/edit/preview.tsx | 78 ++++++++++++ web/components/estimates/points/edit/root.tsx | 84 +++++++++++++ .../estimates/points/{ => edit}/update.tsx | 75 ++++++++---- web/components/estimates/points/index.ts | 6 +- web/components/estimates/root.tsx | 3 +- web/components/estimates/update/index.ts | 1 - web/components/estimates/update/modal.tsx | 75 +++--------- web/components/estimates/update/stage-two.tsx | 111 ------------------ web/services/project/estimate.service.ts | 2 +- web/store/estimates/estimate.ts | 54 +++++++-- web/store/estimates/project-estimate.store.ts | 5 +- 19 files changed, 450 insertions(+), 373 deletions(-) delete mode 100644 web/components/estimates/points/delete.tsx delete mode 100644 web/components/estimates/points/edit-root.tsx rename web/components/estimates/points/{ => edit}/create.tsx (57%) create mode 100644 web/components/estimates/points/edit/delete.tsx create mode 100644 web/components/estimates/points/edit/index.ts create mode 100644 web/components/estimates/points/edit/preview.tsx create mode 100644 web/components/estimates/points/edit/root.tsx rename web/components/estimates/points/{ => edit}/update.tsx (55%) delete mode 100644 web/components/estimates/update/stage-two.tsx diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index d7cc82d32..173e7da49 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -64,9 +64,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): def create(self, request, slug, project_id): estimate = request.data.get('estimate') estimate_name = estimate.get("name", generate_random_name()) + estimate_type = estimate.get("type", 'categories') last_used = estimate.get("last_used", False) estimate = Estimate.objects.create( - name=estimate_name, project_id=project_id, last_used=last_used + name=estimate_name, project_id=project_id, last_used=last_used, type=estimate_type ) estimate_points = request.data.get("estimate_points", []) diff --git a/web/components/estimates/create/modal.tsx b/web/components/estimates/create/modal.tsx index 24296a51c..30cf02ab9 100644 --- a/web/components/estimates/create/modal.tsx +++ b/web/components/estimates/create/modal.tsx @@ -31,7 +31,7 @@ export const CreateEstimateModal: FC = observer((props) => useEffect(() => { if (!isOpen) { - setEstimateSystem(EEstimateSystem.POINTS); + setEstimateSystem(EEstimateSystem.CATEGORIES); setEstimatePoints(undefined); } }, [isOpen]); @@ -42,6 +42,7 @@ export const CreateEstimateModal: FC = observer((props) => const handleCreateEstimate = async () => { try { if (!workspaceSlug || !projectId) return; + const validatedEstimatePoints: TEstimatePointsObject[] = []; if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) { estimatePoints?.map((estimatePoint) => { @@ -56,6 +57,7 @@ export const CreateEstimateModal: FC = observer((props) => if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint); }); } + if (validatedEstimatePoints.length === estimatePoints?.length) { const payload: IEstimateFormData = { estimate: { @@ -80,7 +82,6 @@ export const CreateEstimateModal: FC = observer((props) => }); } } catch (error) { - console.log(error); setToast({ type: TOAST_TYPE.ERROR, title: "Error!", diff --git a/web/components/estimates/create/stage-one.tsx b/web/components/estimates/create/stage-one.tsx index 12486faca..afb4aeca4 100644 --- a/web/components/estimates/create/stage-one.tsx +++ b/web/components/estimates/create/stage-one.tsx @@ -18,7 +18,7 @@ export const EstimateCreateStageOne: FC = (props) => { if (!currentEstimateSystem) return <>; return ( -
+
{ @@ -36,7 +36,8 @@ export const EstimateCreateStageOne: FC = (props) => { className="mb-4" />
-
+ +
Start from scratch
+
+
Choose a template
{Object.keys(currentEstimateSystem.templates).map((name) => diff --git a/web/components/estimates/points/delete.tsx b/web/components/estimates/points/delete.tsx deleted file mode 100644 index 5cca9dab1..000000000 --- a/web/components/estimates/points/delete.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { FC, useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { Check, X } from "lucide-react"; -import { Spinner } from "@plane/ui"; -// constants -import { EEstimateSystem } from "@/constants/estimates"; -// hooks -import { useEstimate, useEstimatePoint } from "@/hooks/store"; - -type TEstimatePointDelete = { - workspaceSlug: string; - projectId: string; - estimateId: string; - estimatePointId: string | undefined; -}; - -export const EstimatePointDelete: FC = observer((props) => { - const { workspaceSlug, projectId, estimateId, estimatePointId } = props; - // hooks - const { asJson: estimate, estimatePointIds } = useEstimate(estimateId); - const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId); - // states - const [loader, setLoader] = useState(false); - const [estimateValue, setEstimateValue] = useState(undefined); - - useEffect(() => { - if (estimateValue === undefined) setEstimateValue(estimatePoint?.value || ""); - }, [estimateValue, estimatePoint]); - - const handleCreate = async () => { - if (estimatePointId) { - if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; - try { - const estimateType: EEstimateSystem | undefined = estimate?.type; - let isEstimateValid = false; - if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) { - if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) { - isEstimateValid = true; - } - } else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) { - if (estimateValue && estimateValue.length > 0) { - isEstimateValid = true; - } - } - - if (isEstimateValid) { - setLoader(true); - const payload = { - key: estimatePointIds?.length + 1, - value: estimateValue, - }; - await updateEstimatePoint(workspaceSlug, projectId, payload); - setLoader(false); - handleClose(); - } else { - console.log("please enter a valid estimate value"); - } - } catch { - setLoader(false); - console.log("something went wrong. please try again later"); - } - } else { - } - }; - - const handleClose = () => { - setEstimateValue(""); - }; - - return ( -
-
- setEstimateValue(e.target.value)} - className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" - /> -
- {loader ? ( -
- -
- ) : ( -
- -
- )} -
- -
-
- ); -}); diff --git a/web/components/estimates/points/edit-root.tsx b/web/components/estimates/points/edit-root.tsx deleted file mode 100644 index 7ff161f68..000000000 --- a/web/components/estimates/points/edit-root.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { FC, useState } from "react"; -import { observer } from "mobx-react"; -import { Draggable } from "@plane/ui"; -// constants -import { EEstimateUpdateStages } from "@/constants/estimates"; - -type TEstimatePointEditRoot = { - workspaceSlug: string; - projectId: string; - estimateId: string; - mode: EEstimateUpdateStages; -}; - -type TEstimatePointEditingState = "update" | "delete"; - -export const EstimatePointEditRoot: FC = observer((props) => { - // props - const { workspaceSlug, projectId, estimateId, mode } = props; - // hooks - // states - const [editingState, setEditingState] = useState(undefined); - - const [estimateEditLoader, setEstimateEditLoader] = useState(false); - const [deletedEstimateValue, setDeletedEstimateValue] = useState(undefined); - const [isEstimateEditing, setIsEstimateEditing] = useState(false); - const [isEstimateDeleting, setIsEstimateDeleting] = useState(false); - - return ; -}); diff --git a/web/components/estimates/points/create.tsx b/web/components/estimates/points/edit/create.tsx similarity index 57% rename from web/components/estimates/points/create.tsx rename to web/components/estimates/points/edit/create.tsx index 1aff50003..0220c4dc6 100644 --- a/web/components/estimates/points/create.tsx +++ b/web/components/estimates/points/edit/create.tsx @@ -1,9 +1,11 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; -import { Check, X } from "lucide-react"; -import { Spinner } from "@plane/ui"; +import { Check, Info, X } from "lucide-react"; +import { Spinner, Tooltip } from "@plane/ui"; // constants import { EEstimateSystem } from "@/constants/estimates"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useEstimate } from "@/hooks/store"; @@ -11,66 +13,89 @@ type TEstimatePointCreate = { workspaceSlug: string; projectId: string; estimateId: string; - estimatePointId: string | undefined; + callback: () => void; }; export const EstimatePointCreate: FC = observer((props) => { - const { workspaceSlug, projectId, estimateId, estimatePointId } = props; + const { workspaceSlug, projectId, estimateId, callback } = props; // hooks const { asJson: estimate, estimatePointIds, creteEstimatePoint } = useEstimate(estimateId); // states const [loader, setLoader] = useState(false); - const [estimateValue, setEstimateValue] = useState(""); + const [estimateInputValue, setEstimateInputValue] = useState(""); + const [error, setError] = useState(undefined); + + const handleClose = () => { + setEstimateInputValue(""); + callback(); + }; const handleCreate = async () => { - if (estimatePointId) { - if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; + if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; + if (estimateInputValue) try { + setLoader(true); + setError(undefined); + const estimateType: EEstimateSystem | undefined = estimate?.type; let isEstimateValid = false; + if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) { - if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) { + if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) { isEstimateValid = true; } } else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) { - if (estimateValue && estimateValue.length > 0) { + if (estimateInputValue && estimateInputValue.length > 0) { isEstimateValid = true; } } if (isEstimateValid) { - setLoader(true); const payload = { key: estimatePointIds?.length + 1, - value: estimateValue, + value: estimateInputValue, }; await creteEstimatePoint(workspaceSlug, projectId, payload); setLoader(false); + setError(undefined); handleClose(); } else { - console.log("please enter a valid estimate value"); + setLoader(false); + setError("please enter a valid estimate value"); } } catch { setLoader(false); - console.log("something went wrong. please try again later"); + setError("something went wrong. please try again later"); } - } else { + else { + setError("Please fill the input field"); } }; - const handleClose = () => { - setEstimateValue(""); - }; - return ( -
-
+
+
setEstimateValue(e.target.value)} + value={estimateInputValue} + onChange={(e) => setEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" + autoFocus /> + {error && ( + <> + +
+ +
+
+ + )}
{loader ? (
diff --git a/web/components/estimates/points/edit/delete.tsx b/web/components/estimates/points/edit/delete.tsx new file mode 100644 index 000000000..559d8b413 --- /dev/null +++ b/web/components/estimates/points/edit/delete.tsx @@ -0,0 +1,111 @@ +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { Info, MoveRight, Trash2, X } from "lucide-react"; +import { Select } from "@headlessui/react"; +import { Spinner, Tooltip } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useEstimate, useEstimatePoint } from "@/hooks/store"; + +type TEstimatePointDelete = { + workspaceSlug: string; + projectId: string; + estimateId: string; + estimatePointId: string; + callback: () => void; +}; + +export const EstimatePointDelete: FC = observer((props) => { + const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props; + // hooks + const { asJson: estimate, deleteEstimatePoint } = useEstimate(estimateId); + const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId); + // states + const [loader, setLoader] = useState(false); + const [estimateInputValue, setEstimateInputValue] = useState(undefined); + const [error, setError] = useState(undefined); + + const handleClose = () => { + setEstimateInputValue(""); + callback(); + }; + + const handleCreate = async () => { + if (!workspaceSlug || !projectId || !projectId) return; + try { + setLoader(true); + setError(undefined); + await deleteEstimatePoint(workspaceSlug, projectId, estimateId, estimateInputValue); + setLoader(false); + setError(undefined); + handleClose(); + } catch { + setLoader(false); + setError("something went wrong. please try again later"); + } + }; + + // derived values + const selectDropdownOptions = + estimate && estimate?.points ? estimate?.points.filter((point) => point.id !== estimatePointId) : []; + + return ( +
+
+
+ {estimatePoint?.value} +
+
+ Mark as +
+
+ + {error && ( + <> + +
+ +
+
+ + )} +
+
+ {loader ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ +
+
+ ); +}); diff --git a/web/components/estimates/points/edit/index.ts b/web/components/estimates/points/edit/index.ts new file mode 100644 index 000000000..9299ca86d --- /dev/null +++ b/web/components/estimates/points/edit/index.ts @@ -0,0 +1,5 @@ +export * from "./root"; +export * from "./preview"; +export * from "./create"; +export * from "./update"; +export * from "./delete"; diff --git a/web/components/estimates/points/edit/preview.tsx b/web/components/estimates/points/edit/preview.tsx new file mode 100644 index 000000000..e7b6e1bf4 --- /dev/null +++ b/web/components/estimates/points/edit/preview.tsx @@ -0,0 +1,78 @@ +import { FC, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { GripVertical, Pencil, Trash2 } from "lucide-react"; +// components +import { EstimatePointUpdate, EstimatePointDelete } from "@/components/estimates/points"; +// hooks +import { useEstimatePoint } from "@/hooks/store"; + +type TEstimatePointItemPreview = { + workspaceSlug: string; + projectId: string; + estimateId: string; + estimatePointId: string; +}; + +export const EstimatePointItemPreview: FC = observer((props) => { + const { workspaceSlug, projectId, estimateId, estimatePointId } = props; + // hooks + const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId); + // state + const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false); + const [estimatePointDeleteToggle, setEstimatePointDeleteToggle] = useState(false); + // ref + const EstimatePointValueRef = useRef(null); + + useEffect(() => { + if (!estimatePointEditToggle && !estimatePointDeleteToggle) + EstimatePointValueRef?.current?.addEventListener("dblclick", () => setEstimatePointEditToggle(true)); + }, [estimatePointDeleteToggle, estimatePointEditToggle]); + + if (!estimatePoint?.id) return <>; + return ( +
+ {!estimatePointEditToggle && !estimatePointDeleteToggle && ( +
+
+ +
+
+ {estimatePoint?.value} +
+
setEstimatePointEditToggle(true)} + > + +
+
setEstimatePointDeleteToggle(true)} + > + +
+
+ )} + + {estimatePoint && estimatePointEditToggle && ( + setEstimatePointEditToggle(false)} + /> + )} + + {estimatePoint && estimatePointDeleteToggle && ( + setEstimatePointDeleteToggle(false)} + /> + )} +
+ ); +}); diff --git a/web/components/estimates/points/edit/root.tsx b/web/components/estimates/points/edit/root.tsx new file mode 100644 index 000000000..9a6cba7d6 --- /dev/null +++ b/web/components/estimates/points/edit/root.tsx @@ -0,0 +1,84 @@ +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { Plus } from "lucide-react"; +import { TEstimatePointsObject } from "@plane/types"; +import { Button, Draggable, Sortable } from "@plane/ui"; +// components +import { EstimatePointItemPreview, EstimatePointCreate } from "@/components/estimates/points"; +// constants +import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates"; +// hooks +import { useEstimate } from "@/hooks/store"; + +type TEstimatePointEditRoot = { + workspaceSlug: string; + projectId: string; + estimateId: string; + mode?: EEstimateUpdateStages; +}; + +export const EstimatePointEditRoot: FC = observer((props) => { + // props + const { workspaceSlug, projectId, estimateId } = props; + // hooks + const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSortOrder } = useEstimate(estimateId); + // states + const [estimatePointCreateToggle, setEstimatePointCreateToggle] = useState(false); + + const estimatePoints: TEstimatePointsObject[] = + estimatePointIds && estimatePointIds.length > 0 + ? (estimatePointIds.map((estimatePointId: string) => { + const estimatePoint = estimatePointById(estimatePointId); + if (estimatePoint) return { id: estimatePointId, key: estimatePoint.key, value: estimatePoint.value }; + }) as TEstimatePointsObject[]) + : ([] as TEstimatePointsObject[]); + + const handleDragEstimatePoints = (updatedEstimatedOrder: TEstimatePointsObject[]) => { + const updatedEstimateKeysOrder = updatedEstimatedOrder.map((item, index) => ({ ...item, key: index + 1 })); + updateEstimateSortOrder(workspaceSlug, projectId, updatedEstimateKeysOrder); + }; + + if (!workspaceSlug || !projectId || !estimateId) return <>; + return ( +
+
{estimate?.type}
+ ( + + {value?.id && ( + + )} + + )} + onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} + keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} + /> + + {estimatePointCreateToggle && ( + setEstimatePointCreateToggle(false)} + /> + )} + {estimatePoints && estimatePoints.length <= maxEstimatesCount && ( + + )} +
+ ); +}); diff --git a/web/components/estimates/points/update.tsx b/web/components/estimates/points/edit/update.tsx similarity index 55% rename from web/components/estimates/points/update.tsx rename to web/components/estimates/points/edit/update.tsx index 149a9aaf3..1e157c13f 100644 --- a/web/components/estimates/points/update.tsx +++ b/web/components/estimates/points/edit/update.tsx @@ -1,9 +1,11 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { Check, X } from "lucide-react"; -import { Spinner } from "@plane/ui"; +import { Check, Info, X } from "lucide-react"; +import { Spinner, Tooltip } from "@plane/ui"; // constants import { EEstimateSystem } from "@/constants/estimates"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useEstimate, useEstimatePoint } from "@/hooks/store"; @@ -11,71 +13,94 @@ type TEstimatePointUpdate = { workspaceSlug: string; projectId: string; estimateId: string; - estimatePointId: string | undefined; + estimatePointId: string; + callback: () => void; }; export const EstimatePointUpdate: FC = observer((props) => { - const { workspaceSlug, projectId, estimateId, estimatePointId } = props; + const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props; // hooks const { asJson: estimate, estimatePointIds } = useEstimate(estimateId); const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId); // states const [loader, setLoader] = useState(false); - const [estimateValue, setEstimateValue] = useState(undefined); + const [estimateInputValue, setEstimateInputValue] = useState(undefined); + const [error, setError] = useState(undefined); useEffect(() => { - if (estimateValue === undefined) setEstimateValue(estimatePoint?.value || ""); - }, [estimateValue, estimatePoint]); + if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || ""); + }, [estimateInputValue, estimatePoint]); + + const handleClose = () => { + setEstimateInputValue(""); + callback(); + }; const handleCreate = async () => { - if (estimatePointId) { - if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; + if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; + if (estimateInputValue) try { + setLoader(true); + setError(undefined); + const estimateType: EEstimateSystem | undefined = estimate?.type; let isEstimateValid = false; + if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) { - if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) { + if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) { isEstimateValid = true; } } else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) { - if (estimateValue && estimateValue.length > 0) { + if (estimateInputValue && estimateInputValue.length > 0) { isEstimateValid = true; } } if (isEstimateValid) { - setLoader(true); const payload = { - key: estimatePointIds?.length + 1, - value: estimateValue, + value: estimateInputValue, }; await updateEstimatePoint(workspaceSlug, projectId, payload); setLoader(false); + setError(undefined); handleClose(); } else { - console.log("please enter a valid estimate value"); + setLoader(false); + setError("please enter a valid estimate value"); } } catch { setLoader(false); - console.log("something went wrong. please try again later"); + setError("something went wrong. please try again later"); } - } else { + else { + setError("Please fill the input field"); } }; - const handleClose = () => { - setEstimateValue(""); - }; - return ( -
-
+
+
setEstimateValue(e.target.value)} + value={estimateInputValue} + onChange={(e) => setEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" + autoFocus /> + {error && ( + <> + +
+ +
+
+ + )}
{loader ? (
diff --git a/web/components/estimates/points/index.ts b/web/components/estimates/points/index.ts index adcc5f475..dbaf88870 100644 --- a/web/components/estimates/points/index.ts +++ b/web/components/estimates/points/index.ts @@ -1,8 +1,4 @@ export * from "./estimate-point-item"; export * from "./inline-editable"; -export * from "./edit-root"; - -export * from "./create"; -export * from "./update"; -export * from "./delete"; +export * from "./edit"; diff --git a/web/components/estimates/root.tsx b/web/components/estimates/root.tsx index 2bea4b87d..c23d4746b 100644 --- a/web/components/estimates/root.tsx +++ b/web/components/estimates/root.tsx @@ -44,6 +44,7 @@ export const EstimateRoot: FC = observer((props) => { Estimates
+ {/* current active estimate section */} {currentActiveEstimateId ? (
{/* estimates activated deactivated section */} @@ -88,7 +89,7 @@ export const EstimateRoot: FC = observer((props) => {
)} - {/* modals for create and update */} + {/* CRUD modals */} = observer((props) => { // props const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props; - // hooks - const { asJson: currentEstimate } = useEstimate(estimateId); // states const [estimateEditType, setEstimateEditType] = useState(undefined); - const [estimatePoints, setEstimatePoints] = useState(undefined); - const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => { - if (currentEstimate?.points && currentEstimate?.points.length > 0) { - let estimateValidatePoints: TEstimatePointsObject[] = []; - currentEstimate?.points.map( - (point) => - point.key && point.value && estimateValidatePoints.push({ id: point.id, key: point.key, value: point.value }) - ); - estimateValidatePoints = orderBy(estimateValidatePoints, ["key"], ["asc"]); - if (estimateValidatePoints.length > 0) { - setEstimateEditType(type); - setEstimatePoints(estimateValidatePoints); - } - } - }; - - const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints); + const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => setEstimateEditType(type); useEffect(() => { - if (!isOpen) { - setEstimateEditType(undefined); - setEstimatePoints(undefined); - } + if (!isOpen) setEstimateEditType(undefined); }, [isOpen]); - // derived values - const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); - return (
@@ -65,10 +35,7 @@ export const UpdateEstimateModal: FC = observer((props) =>
{estimateEditType && (
{ - setEstimateEditType(undefined); - handleUpdatePoints(undefined); - }} + onClick={() => setEstimateEditType(undefined)} className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center" > @@ -76,29 +43,17 @@ export const UpdateEstimateModal: FC = observer((props) => )}
Edit estimate system
-
Step {renderEstimateStepsCount}/2
-
- - {/* estimate steps */} -
- {!estimateEditType && } - {estimateEditType && estimatePoints && ( - - )} -
- -
-
+ +
+ {!estimateEditType && } + {estimateEditType && estimateId && ( + + )} +
); diff --git a/web/components/estimates/update/stage-two.tsx b/web/components/estimates/update/stage-two.tsx deleted file mode 100644 index 9cf754b80..000000000 --- a/web/components/estimates/update/stage-two.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { FC } from "react"; -import cloneDeep from "lodash/cloneDeep"; -import { observer } from "mobx-react"; -import { Plus } from "lucide-react"; -import { IEstimate, TEstimatePointsObject, TEstimateUpdateStageKeys } from "@plane/types"; -import { Button, Sortable } from "@plane/ui"; -// components -import { EstimatePointItem } from "@/components/estimates"; -// constants -import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates"; -import { useEstimate } from "@/hooks/store"; - -type TEstimateUpdateStageTwo = { - workspaceSlug: string; - projectId: string; - estimate: IEstimate; - estimateEditType: TEstimateUpdateStageKeys | undefined; - estimatePoints: TEstimatePointsObject[]; - handleEstimatePoints: (value: TEstimatePointsObject[]) => void; -}; - -export const EstimateUpdateStageTwo: FC = observer((props) => { - const { workspaceSlug, projectId, estimate, estimateEditType, estimatePoints, handleEstimatePoints } = props; - // hooks - const { updateEstimate: updateEstimateRequest } = useEstimate(estimate?.id); - - const currentEstimateSystem = estimate || undefined; - - const addNewEstimationPoint = () => { - const currentEstimationPoints = cloneDeep(estimatePoints); - const newEstimationPoint: TEstimatePointsObject = { - key: currentEstimationPoints.length + 1, - value: "", - }; - handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]); - }; - - const editEstimationPoint = (index: number, value: string) => { - const newEstimationPoints = estimatePoints; - newEstimationPoints[index].value = value; - handleEstimatePoints(newEstimationPoints); - }; - - const deleteEstimationPoint = (index: number) => { - let newEstimationPoints = cloneDeep(estimatePoints); - newEstimationPoints.splice(index, 1); - newEstimationPoints = newEstimationPoints.map((item, index) => ({ - ...item, - key: index + 1, - })); - handleEstimatePoints(newEstimationPoints); - }; - - const replaceEstimateItem = (index: number, value: TEstimatePointsObject) => { - const newEstimationPoints = cloneDeep(estimatePoints); - newEstimationPoints[index].id = value.id; - newEstimationPoints[index].key = value.key; - newEstimationPoints[index].value = value.value; - handleEstimatePoints(newEstimationPoints); - }; - - const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) => { - try { - const sortedEstimatePoints = cloneDeep(updatedEstimatePoints).map((item, index) => ({ - ...item, - key: index + 1, - })) as TEstimatePointsObject[]; - handleEstimatePoints(sortedEstimatePoints); - updateEstimateRequest(workspaceSlug, projectId, { estimate_points: sortedEstimatePoints }); - } catch (error) { - console.log(error); - } - }; - - if (!estimateEditType) return <>; - return ( -
-
- {estimateEditType === EEstimateUpdateStages.SWITCH ? "Estimate type switching" : currentEstimateSystem?.type} -
-
- ( - editEstimationPoint(index, value)} - replaceEstimateItem={(value: TEstimatePointsObject) => replaceEstimateItem(index, value)} - deleteItem={() => deleteEstimationPoint(index)} - /> - )} - onChange={(data: TEstimatePointsObject[]) => updatedSortedKeys(data)} - keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} - /> - {estimateEditType === EEstimateUpdateStages.EDIT && ( - <> - {estimatePoints && estimatePoints.length <= maxEstimatesCount && ( - - )} - - )} -
-
- ); -}); diff --git a/web/services/project/estimate.service.ts b/web/services/project/estimate.service.ts index 1b35d7ff7..b996a7b8a 100644 --- a/web/services/project/estimate.service.ts +++ b/web/services/project/estimate.service.ts @@ -61,7 +61,7 @@ export class EstimateService extends APIService { projectId: string, estimateId: string, payload: Partial - ): Promise { + ): Promise<{ points: IEstimatePoint[] } | undefined> { try { const { data } = await this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, diff --git a/web/store/estimates/estimate.ts b/web/store/estimates/estimate.ts index e2580a5a9..a894b9f80 100644 --- a/web/store/estimates/estimate.ts +++ b/web/store/estimates/estimate.ts @@ -1,3 +1,4 @@ +import orderBy from "lodash/orderBy"; import set from "lodash/set"; import unset from "lodash/unset"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; @@ -7,6 +8,7 @@ import { IEstimatePoint as IEstimatePointType, TEstimateSystemKeys, IEstimateFormData, + TEstimatePointsObject, } from "@plane/types"; // services import { EstimateService } from "@/services/project/estimate.service"; @@ -26,13 +28,18 @@ export interface IEstimate extends IEstimateType { // computed asJson: IEstimateType; estimatePointIds: string[] | undefined; - estimatePointById: (estimateId: string) => IEstimatePointType | undefined; + estimatePointById: (estimatePointId: string) => IEstimatePointType | undefined; // actions updateEstimate: ( workspaceSlug: string, projectId: string, payload: Partial ) => Promise; + updateEstimateSortOrder: ( + workspaceSlug: string, + projectId: string, + payload: TEstimatePointsObject[] + ) => Promise; creteEstimatePoint: ( workspaceSlug: string, projectId: string, @@ -92,6 +99,7 @@ export class Estimate implements IEstimate { estimatePointIds: computed, // actions updateEstimate: action, + updateEstimateSortOrder: action, creteEstimatePoint: action, deleteEstimatePoint: action, }); @@ -136,11 +144,11 @@ export class Estimate implements IEstimate { get estimatePointIds() { const { estimatePoints } = this; if (!estimatePoints) return undefined; - - const estimatePointIds = Object.values(estimatePoints) - .map((point) => point.estimate && this.id) - .filter((id) => id) as string[]; - + let currentEstimatePoints = Object.values(estimatePoints).filter( + (estimatePoint) => estimatePoint?.estimate === this.id + ); + currentEstimatePoints = orderBy(currentEstimatePoints, ["key"], "asc"); + const estimatePointIds = currentEstimatePoints.map((estimatePoint) => estimatePoint.id) as string[]; return estimatePointIds ?? undefined; } @@ -166,16 +174,38 @@ export class Estimate implements IEstimate { if (!this.id || !payload) return; const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload); - if (estimate) { + return estimate as any; + } catch (error) { + throw error; + } + }; + + /** + * @description update an estimate sort order + * @param { string } workspaceSlug + * @param { string } projectId + * @param { Partial } payload + * @returns { void } + */ + updateEstimateSortOrder = async ( + workspaceSlug: string, + projectId: string, + payload: TEstimatePointsObject[] + ): Promise => { + try { + if (!this.id || !payload) return; + + const estimatePoints = await this.service.updateEstimate(workspaceSlug, projectId, this.id, { + estimate_points: payload, + }); + if (estimatePoints?.points && estimatePoints?.points.length > 0) { runInAction(() => { - Object.keys(payload).map((key) => { - const estimateKey = key as keyof IEstimateType; - set(this, estimateKey, estimate[estimateKey]); + estimatePoints?.points.map((estimatePoint) => { + if (estimatePoint.id) + set(this.estimatePoints, [estimatePoint.id], new EstimatePoint(this.store, this.data, estimatePoint)); }); }); } - - return estimate; } catch (error) { throw error; } diff --git a/web/store/estimates/project-estimate.store.ts b/web/store/estimates/project-estimate.store.ts index f028b33b1..49a37451d 100644 --- a/web/store/estimates/project-estimate.store.ts +++ b/web/store/estimates/project-estimate.store.ts @@ -162,6 +162,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { status: "error", message: "Error fetching estimates", }; + throw error; } }; @@ -196,6 +197,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { status: "error", message: "Error fetching estimates", }; + throw error; } }; @@ -231,6 +233,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { status: "error", message: "Error fetching estimate by id", }; + throw error; } }; @@ -250,7 +253,6 @@ export class ProjectEstimateStore implements IProjectEstimateStore { this.error = undefined; const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload); - console.log("estimate", estimate); if (estimate) { await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, { estimate: estimate.id, @@ -266,6 +268,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { status: "error", message: "Error creating estimate", }; + throw error; } }; }