diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts index 9bad7e260..145edf117 100644 --- a/packages/types/src/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -75,3 +75,13 @@ export type TEstimateUpdateStageKeys = | EEstimateUpdateStages.CREATE | EEstimateUpdateStages.EDIT | EEstimateUpdateStages.SWITCH; + +export type TEstimateTypeErrorObject = { + oldValue: string; + newValue: string; + message: string | undefined; +}; + +export type TEstimateTypeError = + | Record + | undefined; diff --git a/web/core/components/estimates/create/modal.tsx b/web/core/components/estimates/create/modal.tsx index 54e606269..0302b1de8 100644 --- a/web/core/components/estimates/create/modal.tsx +++ b/web/core/components/estimates/create/modal.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { ChevronLeft } from "lucide-react"; -import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject } from "@plane/types"; +import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject, TEstimateTypeError } from "@plane/types"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; @@ -28,25 +28,59 @@ export const CreateEstimateModal: FC = observer((props) => // states const [estimateSystem, setEstimateSystem] = useState(EEstimateSystem.POINTS); const [estimatePoints, setEstimatePoints] = useState(undefined); - const [estimatePointCreate, setEstimatePointCreate] = useState(undefined); - const [estimatePointCreateError, setEstimatePointCreateError] = useState([]); + const [estimatePointError, setEstimatePointError] = useState(undefined); const [buttonLoader, setButtonLoader] = useState(false); const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints); + const handleEstimatePointError = ( + key: number, + oldValue: string, + newValue: string, + message: string | undefined, + mode: "add" | "delete" = "add" + ) => { + setEstimatePointError((prev) => { + if (mode === "add") { + return { ...prev, [key]: { oldValue, newValue, message } }; + } else { + const newError = { ...prev }; + delete newError[key]; + return newError; + } + }); + }; + useEffect(() => { if (isOpen) { setEstimateSystem(EEstimateSystem.POINTS); setEstimatePoints(undefined); + setEstimatePointError([]); } }, [isOpen]); + const validateEstimatePointError = () => { + let estimateError = false; + if (!estimatePointError) return estimateError; + + Object.keys(estimatePointError || {}).forEach((key) => { + const currentKey = key as unknown as number; + if ( + estimatePointError[currentKey]?.oldValue != estimatePointError[currentKey]?.newValue || + estimatePointError[currentKey]?.newValue === "" || + estimatePointError[currentKey]?.message + ) { + estimateError = true; + } + }); + + return estimateError; + }; + const handleCreateEstimate = async () => { - setEstimatePointCreateError([]); - if (estimatePointCreate === undefined || estimatePointCreate?.length === 0) { + if (!validateEstimatePointError()) { try { if (!workspaceSlug || !projectId || !estimatePoints) return; - setButtonLoader(true); const payload: IEstimateFormData = { estimate: { @@ -57,7 +91,6 @@ export const CreateEstimateModal: FC = observer((props) => estimate_points: estimatePoints, }; await createEstimate(workspaceSlug, projectId, payload); - setButtonLoader(false); setToast({ type: TOAST_TYPE.SUCCESS, @@ -74,12 +107,32 @@ export const CreateEstimateModal: FC = observer((props) => }); } } else { - setEstimatePointCreateError(estimatePointCreate.map((point) => point.key)); + setEstimatePointError((prev) => { + const newError = { ...prev }; + Object.keys(newError || {}).forEach((key) => { + const currentKey = key as unknown as number; + if ( + newError[currentKey]?.newValue != "" && + newError[currentKey]?.oldValue === newError[currentKey]?.newValue + ) { + delete newError[currentKey]; + } else { + newError[currentKey].message = + newError[currentKey].message || + "Estimate point can't be empty. Enter a value in each field or remove those you don't have values for."; + } + }); + return newError; + }); } }; // derived values const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); + // const isEstimatePointError = useMemo(() => { + // if (!estimatePointError) return false; + // return Object.keys(estimatePointError).length > 0; + // }, [estimatePointError]); return ( @@ -122,20 +175,16 @@ export const CreateEstimateModal: FC = observer((props) => estimateType={estimateSystem} estimatePoints={estimatePoints} setEstimatePoints={setEstimatePoints} - estimatePointCreate={estimatePointCreate} - setEstimatePointCreate={(value) => { - setEstimatePointCreateError([]); - setEstimatePointCreate(value); - }} - estimatePointCreateError={estimatePointCreateError} + estimatePointError={estimatePointError} + handleEstimatePointError={handleEstimatePointError} /> )} - {estimatePointCreateError.length > 0 && ( + {/* {isEstimatePointError && (
Estimate points can't be empty. Enter a value in each field or remove those you don't have values for.
- )} + )} */}
diff --git a/web/core/components/estimates/points/create-root.tsx b/web/core/components/estimates/points/create-root.tsx index 53842fdb6..33e220313 100644 --- a/web/core/components/estimates/points/create-root.tsx +++ b/web/core/components/estimates/points/create-root.tsx @@ -1,9 +1,9 @@ "use client"; -import { Dispatch, FC, SetStateAction, useCallback } from "react"; +import { Dispatch, FC, SetStateAction, useCallback, useState } from "react"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; -import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeError } from "@plane/types"; import { Button, Sortable } from "@plane/ui"; // components import { EstimatePointCreate, EstimatePointItemPreview } from "@/components/estimates/points"; @@ -17,9 +17,14 @@ type TEstimatePointCreateRoot = { estimateType: TEstimateSystemKeys; estimatePoints: TEstimatePointsObject[]; setEstimatePoints: Dispatch>; - estimatePointCreate: TEstimatePointsObject[] | undefined; - setEstimatePointCreate: Dispatch>; - estimatePointCreateError: number[]; + estimatePointError?: TEstimateTypeError; + handleEstimatePointError?: ( + key: number, + oldValue: string, + newValue: string, + message: string | undefined, + mode: "add" | "delete" + ) => void; }; export const EstimatePointCreateRoot: FC = observer((props) => { @@ -31,10 +36,11 @@ export const EstimatePointCreateRoot: FC = observer((p estimateType, estimatePoints, setEstimatePoints, - estimatePointCreate, - setEstimatePointCreate, - estimatePointCreateError, + estimatePointError, + handleEstimatePointError, } = props; + // states + const [estimatePointCreate, setEstimatePointCreate] = useState(undefined); const handleEstimatePoint = useCallback( (mode: "add" | "remove" | "update", value: TEstimatePointsObject) => { @@ -90,11 +96,13 @@ export const EstimatePointCreateRoot: FC = observer((p const handleCreate = () => { if (estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= estimateCount.max - 1) { + const currentKey = estimatePoints.length + (estimatePointCreate?.length || 0) + 1; handleEstimatePointCreate("add", { id: undefined, - key: estimatePoints.length + (estimatePointCreate?.length || 0) + 1, + key: currentKey, value: "", }); + handleEstimatePointError && handleEstimatePointError(currentKey, "", "", undefined, "add"); } }; @@ -119,6 +127,14 @@ export const EstimatePointCreateRoot: FC = observer((p handleEstimatePoint("update", { ...value, value: estimatePointValue }) } handleEstimatePointValueRemove={() => handleEstimatePoint("remove", value)} + estimatePointError={estimatePointError?.[value.key] || undefined} + handleEstimatePointError={( + newValue: string, + message: string | undefined, + mode: "add" | "delete" = "add" + ) => + handleEstimatePointError && handleEstimatePointError(value.key, value.value, newValue, message, mode) + } /> )} onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} @@ -140,7 +156,11 @@ export const EstimatePointCreateRoot: FC = observer((p } closeCallBack={() => handleEstimatePointCreate("remove", estimatePoint)} handleCreateCallback={() => estimatePointCreate.length === 1 && handleCreate()} - isError={estimatePointCreateError.includes(estimatePoint.key) ? true : false} + estimatePointError={estimatePointError?.[estimatePoint.key] || undefined} + handleEstimatePointError={(newValue: string, message: string | undefined, mode: "add" | "delete" = "add") => + handleEstimatePointError && + handleEstimatePointError(estimatePoint.key, estimatePoint.value, newValue, message, mode) + } /> ))} {estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= estimateCount.max - 1 && ( diff --git a/web/core/components/estimates/points/create.tsx b/web/core/components/estimates/points/create.tsx index 0707b6137..e9a7e35dc 100644 --- a/web/core/components/estimates/points/create.tsx +++ b/web/core/components/estimates/points/create.tsx @@ -1,9 +1,9 @@ "use client"; -import { FC, useState, FormEvent, useEffect } from "react"; +import { FC, useState, FormEvent } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; -import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -22,7 +22,8 @@ type TEstimatePointCreate = { handleEstimatePointValue?: (estimateValue: string) => void; closeCallBack: () => void; handleCreateCallback: () => void; - isError: boolean; + estimatePointError?: TEstimateTypeErrorObject | undefined; + handleEstimatePointError?: (newValue: string, message: string | undefined, mode?: "add" | "delete") => void; }; export const EstimatePointCreate: FC = observer((props) => { @@ -35,21 +36,14 @@ export const EstimatePointCreate: FC = observer((props) => handleEstimatePointValue, closeCallBack, handleCreateCallback, - isError, + estimatePointError, + handleEstimatePointError, } = props; // hooks const { creteEstimatePoint } = useEstimate(estimateId); // states const [estimateInputValue, setEstimateInputValue] = useState(""); const [loader, setLoader] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - if (isError && error === undefined && estimateInputValue.length > 0) { - setError("Confirm this value first or discard it."); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isError]); const handleSuccess = (value: string) => { handleEstimatePointValue && handleEstimatePointValue(value); @@ -58,13 +52,14 @@ export const EstimatePointCreate: FC = observer((props) => }; const handleClose = () => { + handleEstimatePointError && handleEstimatePointError(estimateInputValue, undefined, "delete"); setEstimateInputValue(""); closeCallBack(); }; const handleEstimateInputValue = (value: string) => { - setError(undefined); setEstimateInputValue(value); + handleEstimatePointError && handleEstimatePointError(value, undefined); }; const handleCreate = async (event: FormEvent) => { @@ -72,7 +67,7 @@ export const EstimatePointCreate: FC = observer((props) => if (!workspaceSlug || !projectId) return; - setError(undefined); + handleEstimatePointError && handleEstimatePointError(estimateInputValue, undefined, "delete"); if (estimateInputValue) { const currentEstimateType: EEstimateSystem | undefined = estimateType; @@ -91,7 +86,7 @@ export const EstimatePointCreate: FC = observer((props) => isEstimateValid = true; } } else if (currentEstimateType && currentEstimateType === EEstimateSystem.CATEGORIES) { - if (estimateInputValue && estimateInputValue.length > 0) { + if (estimateInputValue && estimateInputValue.length > 0 && Number(estimateInputValue) < 0) { isEstimateValid = true; } } @@ -108,7 +103,7 @@ export const EstimatePointCreate: FC = observer((props) => await creteEstimatePoint(workspaceSlug, projectId, payload); setLoader(false); - setError(undefined); + handleEstimatePointError && handleEstimatePointError(estimateInputValue, undefined, "delete"); setToast({ type: TOAST_TYPE.SUCCESS, title: "Estimate point created", @@ -117,7 +112,11 @@ export const EstimatePointCreate: FC = observer((props) => handleClose(); } catch { setLoader(false); - setError("We are unable to process your request, please try again."); + handleEstimatePointError && + handleEstimatePointError( + estimateInputValue, + "We are unable to process your request, please try again." + ); setToast({ type: TOAST_TYPE.ERROR, title: "Estimate point creation failed", @@ -132,22 +131,24 @@ export const EstimatePointCreate: FC = observer((props) => } } else { setLoader(false); - setError( - [EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType) - ? "Estimate point needs to be a numeric value." - : "Estimate point needs to be a character value." - ); + handleEstimatePointError && + handleEstimatePointError( + estimateInputValue, + [EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType) + ? "Estimate point needs to be a numeric value." + : "Estimate point needs to be a character value." + ); } - } else setError("Estimate value already exists."); - } else setError("Estimate value cannot be empty."); + } else handleEstimatePointError && handleEstimatePointError(estimateInputValue, "Estimate value already exists."); + } else handleEstimatePointError && handleEstimatePointError(estimateInputValue, "Estimate value cannot be empty."); }; return ( -
+
= observer((props) => placeholder="Enter estimate point" autoFocus /> - {error && ( - + {estimatePointError?.message && ( +
diff --git a/web/core/components/estimates/points/preview.tsx b/web/core/components/estimates/points/preview.tsx index e8f27e00d..a3e24ff04 100644 --- a/web/core/components/estimates/points/preview.tsx +++ b/web/core/components/estimates/points/preview.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { GripVertical, Pencil, Trash2 } from "lucide-react"; -import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; // components import { EstimatePointUpdate, EstimatePointDelete } from "@/components/estimates/points"; // plane web constants @@ -17,6 +17,8 @@ type TEstimatePointItemPreview = { estimatePoints: TEstimatePointsObject[]; handleEstimatePointValueUpdate?: (estimateValue: string) => void; handleEstimatePointValueRemove?: () => void; + estimatePointError?: TEstimateTypeErrorObject | undefined; + handleEstimatePointError?: (newValue: string, message: string | undefined) => void; }; export const EstimatePointItemPreview: FC = observer((props) => { @@ -30,6 +32,8 @@ export const EstimatePointItemPreview: FC = observer( estimatePoints, handleEstimatePointValueUpdate, handleEstimatePointValueRemove, + estimatePointError, + handleEstimatePointError, } = props; // state const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false); @@ -90,6 +94,8 @@ export const EstimatePointItemPreview: FC = observer( handleEstimatePointValueUpdate && handleEstimatePointValueUpdate(estimatePointValue) } closeCallBack={() => setEstimatePointEditToggle(false)} + estimatePointError={estimatePointError} + handleEstimatePointError={handleEstimatePointError} /> )} diff --git a/web/core/components/estimates/points/update.tsx b/web/core/components/estimates/points/update.tsx index ccb0f92da..e20cbcfb1 100644 --- a/web/core/components/estimates/points/update.tsx +++ b/web/core/components/estimates/points/update.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useState, FormEvent } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; -import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -23,6 +23,8 @@ type TEstimatePointUpdate = { estimatePoint: TEstimatePointsObject; handleEstimatePointValueUpdate: (estimateValue: string) => void; closeCallBack: () => void; + estimatePointError?: TEstimateTypeErrorObject | undefined; + handleEstimatePointError?: (newValue: string, message: string | undefined, mode?: "add" | "delete") => void; }; export const EstimatePointUpdate: FC = observer((props) => { @@ -36,13 +38,14 @@ export const EstimatePointUpdate: FC = observer((props) => estimatePoint, handleEstimatePointValueUpdate, closeCallBack, + estimatePointError, + handleEstimatePointError, } = props; // hooks const { updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId); // states const [loader, setLoader] = useState(false); const [estimateInputValue, setEstimateInputValue] = useState(undefined); - const [error, setError] = useState(undefined); useEffect(() => { if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || ""); @@ -60,7 +63,7 @@ export const EstimatePointUpdate: FC = observer((props) => }; const handleEstimateInputValue = (value: string) => { - setError(undefined); + handleEstimatePointError && handleEstimatePointError(value, undefined); setEstimateInputValue(() => value); }; @@ -69,7 +72,7 @@ export const EstimatePointUpdate: FC = observer((props) => if (!workspaceSlug || !projectId) return; - setError(undefined); + handleEstimatePointError && handleEstimatePointError(estimateInputValue || "", undefined, "delete"); if (estimateInputValue) { const currentEstimateType: EEstimateSystem | undefined = estimateType; @@ -97,7 +100,8 @@ export const EstimatePointUpdate: FC = observer((props) => if (estimateId != undefined) { if (estimateInputValue === estimatePoint.value) { setLoader(false); - setError(undefined); + handleEstimatePointError && handleEstimatePointError(estimateInputValue, undefined); + handleClose(); } else try { @@ -109,7 +113,7 @@ export const EstimatePointUpdate: FC = observer((props) => await updateEstimatePoint(workspaceSlug, projectId, payload); setLoader(false); - setError(undefined); + handleEstimatePointError && handleEstimatePointError(estimateInputValue, undefined, "delete"); handleClose(); setToast({ type: TOAST_TYPE.SUCCESS, @@ -118,7 +122,11 @@ export const EstimatePointUpdate: FC = observer((props) => }); } catch { setLoader(false); - setError("We are unable to process your request, please try again."); + handleEstimatePointError && + handleEstimatePointError( + estimateInputValue, + "We are unable to process your request, please try again." + ); setToast({ type: TOAST_TYPE.ERROR, title: "Estimate modification failed", @@ -130,22 +138,25 @@ export const EstimatePointUpdate: FC = observer((props) => } } else { setLoader(false); - setError( - [EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType) - ? "Estimate point needs to be a numeric value." - : "Estimate point needs to be a character value." - ); + handleEstimatePointError && + handleEstimatePointError( + estimateInputValue, + [EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType) + ? "Estimate point needs to be a numeric value." + : "Estimate point needs to be a character value." + ); } - } else setError("Estimate value already exists."); - } else setError("Estimate value cannot be empty."); + } else handleEstimatePointError && handleEstimatePointError(estimateInputValue, "Estimate value already exists."); + } else + handleEstimatePointError && handleEstimatePointError(estimateInputValue || "", "Estimate value cannot be empty."); }; return ( - +
= observer((props) => placeholder="Enter estimate point" autoFocus /> - {error && ( + {estimatePointError?.message && ( <> - +