diff --git a/web/components/core/modals/modal-core.tsx b/web/components/core/modals/modal-core.tsx index 5d24df01c..be1b2ad05 100644 --- a/web/components/core/modals/modal-core.tsx +++ b/web/components/core/modals/modal-core.tsx @@ -17,7 +17,7 @@ export enum EModalWidth { type Props = { children: React.ReactNode; - handleClose: () => void; + handleClose?: () => void; isOpen: boolean; position?: EModalPosition; width?: EModalWidth; @@ -27,7 +27,7 @@ export const ModalCore: React.FC = (props) => { return ( - + handleClose && handleClose}> = observer((props) => // states const [estimateSystem, setEstimateSystem] = useState(EEstimateSystem.POINTS); const [estimatePoints, setEstimatePoints] = useState(undefined); + const [estimatePointCreate, setEstimatePointCreate] = useState(undefined); + const [estimatePointCreateError, setEstimatePointCreateError] = useState([]); const [buttonLoader, setButtonLoader] = useState(false); const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints); @@ -40,33 +42,39 @@ export const CreateEstimateModal: FC = observer((props) => }, [isOpen]); const handleCreateEstimate = async () => { - try { - if (!workspaceSlug || !projectId || !estimatePoints) return; - setButtonLoader(true); - const payload: IEstimateFormData = { - estimate: { - name: ESTIMATE_SYSTEMS[estimateSystem]?.name, - type: estimateSystem, - last_used: true, - }, - estimate_points: estimatePoints, - }; - await createEstimate(workspaceSlug, projectId, payload); + setEstimatePointCreateError([]); + if (estimatePointCreate === undefined || estimatePointCreate?.length === 0) { + try { + if (!workspaceSlug || !projectId || !estimatePoints) return; - setButtonLoader(false); - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Estimate created", - message: "A new estimate has been added in your project.", - }); - handleClose(); - } catch (error) { - setButtonLoader(false); - setToast({ - type: TOAST_TYPE.ERROR, - title: "Estimate creation failed", - message: "We were unable to create the new estimate, please try again.", - }); + setButtonLoader(true); + const payload: IEstimateFormData = { + estimate: { + name: ESTIMATE_SYSTEMS[estimateSystem]?.name, + type: estimateSystem, + last_used: true, + }, + estimate_points: estimatePoints, + }; + await createEstimate(workspaceSlug, projectId, payload); + + setButtonLoader(false); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Estimate created", + message: "A new estimate has been added in your project.", + }); + handleClose(); + } catch (error) { + setButtonLoader(false); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Estimate creation failed", + message: "We were unable to create the new estimate, please try again.", + }); + } + } else { + setEstimatePointCreateError(estimatePointCreate.map((point) => point.key)); } }; @@ -74,7 +82,7 @@ export const CreateEstimateModal: FC = observer((props) => const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); return ( - +
{/* heading */}
@@ -90,7 +98,7 @@ export const CreateEstimateModal: FC = observer((props) =>
)} -
New Estimate System
+
New estimate system
Step {renderEstimateStepsCount} of 2
@@ -107,16 +115,26 @@ export const CreateEstimateModal: FC = observer((props) => /> )} {estimatePoints && ( - <> - - + { + setEstimatePointCreateError([]); + setEstimatePointCreate(value); + }} + estimatePointCreateError={estimatePointCreateError} + /> + )} + {estimatePointCreateError.length > 0 && ( +
+ 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/components/estimates/delete/modal.tsx b/web/components/estimates/delete/modal.tsx index 6bca3d042..b7e8df153 100644 --- a/web/components/estimates/delete/modal.tsx +++ b/web/components/estimates/delete/modal.tsx @@ -53,7 +53,7 @@ export const DeleteEstimateModal: FC = observer((props) => }; return ( - +
{/* heading */}
diff --git a/web/components/estimates/points/create-root.tsx b/web/components/estimates/points/create-root.tsx index e7d3b525c..969bef1ff 100644 --- a/web/components/estimates/points/create-root.tsx +++ b/web/components/estimates/points/create-root.tsx @@ -1,6 +1,6 @@ "use client"; -import { Dispatch, FC, SetStateAction, useCallback, useState } from "react"; +import { Dispatch, FC, SetStateAction, useCallback } from "react"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; @@ -17,13 +17,24 @@ type TEstimatePointCreateRoot = { estimateType: TEstimateSystemKeys; estimatePoints: TEstimatePointsObject[]; setEstimatePoints: Dispatch>; + estimatePointCreate: TEstimatePointsObject[] | undefined; + setEstimatePointCreate: Dispatch>; + estimatePointCreateError: number[]; }; export const EstimatePointCreateRoot: FC = observer((props) => { // props - const { workspaceSlug, projectId, estimateId, estimateType, estimatePoints, setEstimatePoints } = props; - // states - const [estimatePointCreate, setEstimatePointCreate] = useState(undefined); + const { + workspaceSlug, + projectId, + estimateId, + estimateType, + estimatePoints, + setEstimatePoints, + estimatePointCreate, + setEstimatePointCreate, + estimatePointCreateError, + } = props; const handleEstimatePoint = useCallback( (mode: "add" | "remove" | "update", value: TEstimatePointsObject) => { @@ -77,9 +88,19 @@ export const EstimatePointCreateRoot: FC = observer((p setEstimatePoints(() => updatedEstimateKeysOrder); }; + const handleCreate = () => { + if (estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= maxEstimatesCount - 1) { + handleEstimatePointCreate("add", { + id: undefined, + key: estimatePoints.length + (estimatePointCreate?.length || 0) + 1, + value: "", + }); + } + }; + if (!workspaceSlug || !projectId) return <>; return ( -
+
{estimateType}
@@ -118,21 +139,12 @@ export const EstimatePointCreateRoot: FC = observer((p handleEstimatePoint("add", { ...estimatePoint, value: estimatePointValue }) } closeCallBack={() => handleEstimatePointCreate("remove", estimatePoint)} + handleCreateCallback={() => estimatePointCreate.length === 1 && handleCreate()} + isError={estimatePointCreateError.includes(estimatePoint.key) ? true : false} /> ))} - {estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= maxEstimatesCount && ( - )} diff --git a/web/components/estimates/points/create.tsx b/web/components/estimates/points/create.tsx index 7049dce82..0816575b3 100644 --- a/web/components/estimates/points/create.tsx +++ b/web/components/estimates/points/create.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, MouseEvent, FocusEvent, useState } from "react"; +import { FC, useState, FormEvent, useEffect } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; @@ -21,6 +21,8 @@ type TEstimatePointCreate = { estimatePoints: TEstimatePointsObject[]; handleEstimatePointValue?: (estimateValue: string) => void; closeCallBack: () => void; + handleCreateCallback: () => void; + isError: boolean; }; export const EstimatePointCreate: FC = observer((props) => { @@ -32,6 +34,8 @@ export const EstimatePointCreate: FC = observer((props) => estimatePoints, handleEstimatePointValue, closeCallBack, + handleCreateCallback, + isError, } = props; // hooks const { creteEstimatePoint } = useEstimate(estimateId); @@ -40,6 +44,13 @@ export const EstimatePointCreate: FC = observer((props) => 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); setEstimateInputValue(""); @@ -51,7 +62,12 @@ export const EstimatePointCreate: FC = observer((props) => closeCallBack(); }; - const handleCreate = async (event: MouseEvent | FocusEvent) => { + const handleEstimateInputValue = (value: string) => { + setError(undefined); + setEstimateInputValue(value); + }; + + const handleCreate = async (event: FormEvent) => { event.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -110,6 +126,9 @@ export const EstimatePointCreate: FC = observer((props) => } } else { handleSuccess(estimateInputValue); + if (handleCreateCallback) { + handleCreateCallback(); + } } } else { setLoader(false); @@ -124,7 +143,7 @@ export const EstimatePointCreate: FC = observer((props) => }; return ( -
+
= observer((props) => setEstimateInputValue(e.target.value)} + onChange={(e) => handleEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" placeholder="Enter estimate point" autoFocus - onBlur={(e) => !estimateId && handleCreate(e)} /> {error && ( - <> - -
- -
-
- + +
+ +
+
)}
- {estimateId && ( - <> - - - + {estimateInputValue && estimateInputValue.length > 0 && ( + )} +
); }); diff --git a/web/components/estimates/points/preview.tsx b/web/components/estimates/points/preview.tsx index 741b401de..bfd9bd9dc 100644 --- a/web/components/estimates/points/preview.tsx +++ b/web/components/estimates/points/preview.tsx @@ -4,6 +4,7 @@ import { GripVertical, Pencil, Trash2 } from "lucide-react"; import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; // components import { EstimatePointUpdate, EstimatePointDelete } from "@/components/estimates/points"; +import { minEstimatesCount } from "@/constants/estimates"; type TEstimatePointItemPreview = { workspaceSlug: string; @@ -60,16 +61,18 @@ export const EstimatePointItemPreview: FC = observer( >
-
- estimateId && estimatePointId - ? setEstimatePointDeleteToggle(true) - : handleEstimatePointValueRemove && handleEstimatePointValueRemove() - } - > - -
+ {estimatePoints.length > minEstimatesCount && ( +
+ estimateId && estimatePointId + ? setEstimatePointDeleteToggle(true) + : handleEstimatePointValueRemove && handleEstimatePointValueRemove() + } + > + +
+ )}
)} diff --git a/web/components/estimates/points/update.tsx b/web/components/estimates/points/update.tsx index 409a98642..faf55f522 100644 --- a/web/components/estimates/points/update.tsx +++ b/web/components/estimates/points/update.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, MouseEvent, useEffect, FocusEvent, useState } from "react"; +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"; @@ -59,7 +59,12 @@ export const EstimatePointUpdate: FC = observer((props) => closeCallBack(); }; - const handleUpdate = async (event: MouseEvent | FocusEvent) => { + const handleEstimateInputValue = (value: string) => { + setError(undefined); + setEstimateInputValue(() => value); + }; + + const handleUpdate = async (event: FormEvent) => { event.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -71,7 +76,7 @@ export const EstimatePointUpdate: FC = observer((props) => let isEstimateValid = false; const currentEstimatePointValues = estimatePoints - .map((point) => (point?.id != estimatePoint?.id ? point?.value : undefined)) + .map((point) => (point?.key != estimatePoint?.key ? point?.value : undefined)) .filter((value) => value != undefined) as string[]; const isRepeated = (estimateType && isEstimatePointValuesRepeated(currentEstimatePointValues, estimateType, estimateInputValue)) || @@ -136,7 +141,7 @@ export const EstimatePointUpdate: FC = observer((props) => }; return ( -
+
= observer((props) => setEstimateInputValue(e.target.value)} + onChange={(e) => handleEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" placeholder="Enter estimate point" - onBlur={(e) => !estimateId && handleUpdate(e)} autoFocus /> {error && ( @@ -162,25 +166,24 @@ export const EstimatePointUpdate: FC = observer((props) => )}
- {estimateId && ( - <> - - - + + {estimateInputValue && estimateInputValue.length > 0 && ( + )} +
); }); diff --git a/web/components/estimates/update/modal.tsx b/web/components/estimates/update/modal.tsx index 6ad64e7f6..2ffde8ca5 100644 --- a/web/components/estimates/update/modal.tsx +++ b/web/components/estimates/update/modal.tsx @@ -20,7 +20,7 @@ export const UpdateEstimateModal: FC = observer((props) => const { isOpen, handleClose } = props; return ( - +
{/* heading */}
diff --git a/web/constants/estimates.ts b/web/constants/estimates.ts index f9cc58928..4d55bd1bc 100644 --- a/web/constants/estimates.ts +++ b/web/constants/estimates.ts @@ -13,7 +13,8 @@ export enum EEstimateUpdateStages { SWITCH = "switch", } -export const maxEstimatesCount = 11; +export const minEstimatesCount = 2; +export const maxEstimatesCount = 6; export const ESTIMATE_SYSTEMS: TEstimateSystems = { points: { @@ -28,7 +29,6 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = { { id: undefined, key: 4, value: "5" }, { id: undefined, key: 5, value: "8" }, { id: undefined, key: 6, value: "13" }, - { id: undefined, key: 7, value: "21" }, ], }, linear: { @@ -40,10 +40,6 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = { { id: undefined, key: 4, value: "4" }, { id: undefined, key: 5, value: "5" }, { id: undefined, key: 6, value: "6" }, - { id: undefined, key: 7, value: "7" }, - { id: undefined, key: 8, value: "8" }, - { id: undefined, key: 9, value: "9" }, - { id: undefined, key: 10, value: "10" }, ], }, squares: { @@ -116,10 +112,6 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = { { id: undefined, key: 4, value: "4" }, { id: undefined, key: 5, value: "5" }, { id: undefined, key: 6, value: "6" }, - { id: undefined, key: 7, value: "7" }, - { id: undefined, key: 8, value: "8" }, - { id: undefined, key: 9, value: "9" }, - { id: undefined, key: 10, value: "10" }, ], }, },