From c2e07c6b7c0738f64c076ad83581d1beb62642df Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 29 May 2024 17:19:14 +0530 Subject: [PATCH] chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point --- web/components/estimates/create/modal.tsx | 6 +- .../estimates/points/edit/create.tsx | 42 +++++-- .../estimates/points/edit/delete.tsx | 93 ++++++-------- web/components/estimates/points/edit/index.ts | 1 + .../estimates/points/edit/select-dropdown.tsx | 119 ++++++++++++++++++ .../estimates/points/edit/update.tsx | 44 +++++-- web/helpers/estimates.ts | 13 +- web/package.json | 2 +- yarn.lock | 2 +- 9 files changed, 235 insertions(+), 87 deletions(-) create mode 100644 web/components/estimates/points/edit/select-dropdown.tsx diff --git a/web/components/estimates/create/modal.tsx b/web/components/estimates/create/modal.tsx index 577421baf..e06a1f782 100644 --- a/web/components/estimates/create/modal.tsx +++ b/web/components/estimates/create/modal.tsx @@ -62,7 +62,6 @@ export const CreateEstimateModal: FC = observer((props) => estimatePoints.map((point) => point.value), estimateSystem ); - console.log("isRepeated", isRepeated); if (!isRepeated) { const payload: IEstimateFormData = { estimate: { @@ -80,6 +79,11 @@ export const CreateEstimateModal: FC = observer((props) => }); handleClose(); } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate point values cannot be repeated", + }); } } else { setToast({ diff --git a/web/components/estimates/points/edit/create.tsx b/web/components/estimates/points/edit/create.tsx index 2c6561021..154c241d8 100644 --- a/web/components/estimates/points/edit/create.tsx +++ b/web/components/estimates/points/edit/create.tsx @@ -1,11 +1,12 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; -import { Spinner, Tooltip } from "@plane/ui"; +import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // constants import { EEstimateSystem } from "@/constants/estimates"; // helpers import { cn } from "@/helpers/common.helper"; +import { isEstimatePointValuesRepeated } from "@/helpers/estimates"; // hooks import { useEstimate } from "@/hooks/store"; @@ -19,7 +20,7 @@ type TEstimatePointCreate = { export const EstimatePointCreate: FC = observer((props) => { const { workspaceSlug, projectId, estimateId, callback } = props; // hooks - const { asJson: estimate, estimatePointIds, creteEstimatePoint } = useEstimate(estimateId); + const { asJson: estimate, estimatePointIds, estimatePointById, creteEstimatePoint } = useEstimate(estimateId); // states const [loader, setLoader] = useState(false); const [estimateInputValue, setEstimateInputValue] = useState(""); @@ -50,18 +51,35 @@ export const EstimatePointCreate: FC = observer((props) => } } - if (isEstimateValid) { - const payload = { - key: estimatePointIds?.length + 1, - value: estimateInputValue, - }; - await creteEstimatePoint(workspaceSlug, projectId, payload); - setLoader(false); - setError(undefined); - handleClose(); + const currentEstimatePointValues = estimatePointIds + .map((estimatePointId) => estimatePointById(estimatePointId)?.value || undefined) + .filter((estimateValue) => estimateValue != undefined) as string[]; + const isRepeated = + (estimateType && + isEstimatePointValuesRepeated(currentEstimatePointValues, estimateType, estimateInputValue)) || + false; + + if (!isRepeated) { + if (isEstimateValid) { + const payload = { + key: estimatePointIds?.length + 1, + value: estimateInputValue, + }; + await creteEstimatePoint(workspaceSlug, projectId, payload); + setLoader(false); + setError(undefined); + handleClose(); + } else { + setLoader(false); + setError("please enter a valid estimate value"); + } } else { setLoader(false); - setError("please enter a valid estimate value"); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate point values cannot be repeated", + }); } } catch { setLoader(false); diff --git a/web/components/estimates/points/edit/delete.tsx b/web/components/estimates/points/edit/delete.tsx index c13983d59..e81a4a0b8 100644 --- a/web/components/estimates/points/edit/delete.tsx +++ b/web/components/estimates/points/edit/delete.tsx @@ -1,10 +1,10 @@ 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"; +import { MoveRight, Trash2, X } from "lucide-react"; +import { TEstimatePointsObject } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { EstimatePointDropdown } from "@/components/estimates/points"; // hooks import { useEstimate, useEstimatePoint } from "@/hooks/store"; @@ -31,28 +31,37 @@ export const EstimatePointDelete: FC = observer((props) => callback(); }; - const handleCreate = async () => { + const handleDelete = async () => { if (!workspaceSlug || !projectId || !projectId) return; - try { - setLoader(true); - setError(undefined); - await deleteEstimatePoint(workspaceSlug, projectId, estimatePointId, estimateInputValue); - setLoader(false); - setError(undefined); - handleClose(); - } catch { - setLoader(false); - setError("something went wrong. please try again later"); - } + if (estimateInputValue) + try { + setLoader(true); + setError(undefined); + await deleteEstimatePoint( + workspaceSlug, + projectId, + estimatePointId, + estimateInputValue === "none" ? undefined : estimateInputValue + ); + setLoader(false); + setError(undefined); + handleClose(); + } catch { + setLoader(false); + setError("something went wrong. please try again later"); + } + else setError("please select option"); }; // derived values const selectDropdownOptionIds = estimatePointIds?.filter((pointId) => pointId != estimatePointId) as string[]; - const selectDropdownOptions = (selectDropdownOptionIds || [])?.map((pointId) => { - const estimatePoint = estimatePointById(pointId); - if (estimatePoint && estimatePoint?.id) - return { id: estimatePoint.id, key: estimatePoint.key, value: estimatePoint.value }; - }); + const selectDropdownOptions = (selectDropdownOptionIds || []) + ?.map((pointId) => { + const estimatePoint = estimatePointById(pointId); + if (estimatePoint && estimatePoint?.id) + return { id: estimatePoint.id, key: estimatePoint.key, value: estimatePoint.value }; + }) + .filter((estimatePoint) => estimatePoint != undefined) as TEstimatePointsObject[]; return (
@@ -60,37 +69,17 @@ export const EstimatePointDelete: FC = observer((props) =>
{estimatePoint?.value}
-
+
Mark as
-
- - {error && ( - <> - -
- -
-
- - )} -
+ { + setEstimateInputValue(estimateId); + setError(undefined); + }} + />
{loader ? (
@@ -99,7 +88,7 @@ export const EstimatePointDelete: FC = observer((props) => ) : (
diff --git a/web/components/estimates/points/edit/index.ts b/web/components/estimates/points/edit/index.ts index 9299ca86d..9a99010b5 100644 --- a/web/components/estimates/points/edit/index.ts +++ b/web/components/estimates/points/edit/index.ts @@ -3,3 +3,4 @@ export * from "./preview"; export * from "./create"; export * from "./update"; export * from "./delete"; +export * from "./select-dropdown"; diff --git a/web/components/estimates/points/edit/select-dropdown.tsx b/web/components/estimates/points/edit/select-dropdown.tsx new file mode 100644 index 000000000..cac0239de --- /dev/null +++ b/web/components/estimates/points/edit/select-dropdown.tsx @@ -0,0 +1,119 @@ +import { FC, useRef, Fragment, useState } from "react"; +import { Info, Check, ChevronDown } from "lucide-react"; +import { Listbox, ListboxButton, ListboxOptions, Transition, ListboxOption } from "@headlessui/react"; +import { TEstimatePointsObject } from "@plane/types"; +import { Tooltip } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import useDynamicDropdownPosition from "@/hooks/use-dynamic-dropdown"; +import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; + +type TEstimatePointDropdown = { + options: TEstimatePointsObject[]; + error: string | undefined; + callback: (estimateId: string) => void; +}; + +export const EstimatePointDropdown: FC = (props) => { + const { options, error, callback } = props; + // states + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState(undefined); + // ref + const dropdownContainerRef = useRef(null); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + + useDynamicDropdownPosition(isDropdownOpen, () => setIsDropdownOpen(false), buttonRef, dropdownRef); + useOutsideClickDetector(dropdownContainerRef, () => setIsDropdownOpen(false)); + + // derived values + const selectedValue = options.find((option) => option?.id === selectedOption) || undefined; + + return ( +
+ { + setSelectedOption(selectedOption); + callback(selectedOption); + setIsDropdownOpen(false); + }} + className="w-full flex-shrink-0 text-left" + > + setIsDropdownOpen((prev) => !prev)} + className={cn( + "relative w-full rounded border flex items-center gap-3 p-2.5", + error ? `border-red-500` : `border-custom-border-200` + )} + > +
+ {selectedValue?.value || "Select an estimate point"} +
+ + {error && ( + <> + +
+ +
+
+ + )} +
+ + +
+ +
+
None
+ {selectedOption === "none" && } +
+
+ {options.map((option) => ( + +
+
{option.value}
+ {selectedOption === option?.id && } +
+
+ ))} +
+
+
+
+
+ ); +}; diff --git a/web/components/estimates/points/edit/update.tsx b/web/components/estimates/points/edit/update.tsx index 44aeb92da..9ce737416 100644 --- a/web/components/estimates/points/edit/update.tsx +++ b/web/components/estimates/points/edit/update.tsx @@ -1,11 +1,12 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; -import { Spinner, Tooltip } from "@plane/ui"; +import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // constants import { EEstimateSystem } from "@/constants/estimates"; // helpers import { cn } from "@/helpers/common.helper"; +import { isEstimatePointValuesRepeated } from "@/helpers/estimates"; // hooks import { useEstimate, useEstimatePoint } from "@/hooks/store"; @@ -20,7 +21,7 @@ type TEstimatePointUpdate = { export const EstimatePointUpdate: FC = observer((props) => { const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props; // hooks - const { asJson: estimate, estimatePointIds } = useEstimate(estimateId); + const { asJson: estimate, estimatePointIds, estimatePointById } = useEstimate(estimateId); const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId); // states const [loader, setLoader] = useState(false); @@ -36,7 +37,7 @@ export const EstimatePointUpdate: FC = observer((props) => callback(); }; - const handleCreate = async () => { + const handleUpdate = async () => { if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return; if (estimateInputValue) try { @@ -56,17 +57,34 @@ export const EstimatePointUpdate: FC = observer((props) => } } - if (isEstimateValid) { - const payload = { - value: estimateInputValue, - }; - await updateEstimatePoint(workspaceSlug, projectId, payload); - setLoader(false); - setError(undefined); - handleClose(); + const currentEstimatePointValues = estimatePointIds + .map((estimatePointId) => estimatePointById(estimatePointId)?.value || undefined) + .filter((estimateValue) => estimateValue != undefined) as string[]; + const isRepeated = + (estimateType && + isEstimatePointValuesRepeated(currentEstimatePointValues, estimateType, estimateInputValue)) || + false; + + if (!isRepeated) { + if (isEstimateValid) { + const payload = { + value: estimateInputValue, + }; + await updateEstimatePoint(workspaceSlug, projectId, payload); + setLoader(false); + setError(undefined); + handleClose(); + } else { + setLoader(false); + setError("please enter a valid estimate value"); + } } else { setLoader(false); - setError("please enter a valid estimate value"); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate point values cannot be repeated", + }); } } catch { setLoader(false); @@ -110,7 +128,7 @@ export const EstimatePointUpdate: FC = observer((props) => ) : (
diff --git a/web/helpers/estimates.ts b/web/helpers/estimates.ts index 94ddca538..cf8cd2dc8 100644 --- a/web/helpers/estimates.ts +++ b/web/helpers/estimates.ts @@ -1,34 +1,33 @@ import { EEstimateSystem } from "@/constants/estimates"; - export const isEstimatePointValuesRepeated = ( estimatePoints: string[], estimateType: EEstimateSystem, newEstimatePoint?: string | undefined ) => { const currentEstimatePoints = estimatePoints.map((estimatePoint) => estimatePoint.trim()); - let isValid = false; + let isRepeated = false; if (newEstimatePoint === undefined) { if (estimateType === EEstimateSystem.CATEGORIES) { const points = new Set(currentEstimatePoints); - if (points.size === currentEstimatePoints.length) isValid = true; + if (points.size != currentEstimatePoints.length) isRepeated = true; } else if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType)) { currentEstimatePoints.map((point) => { - if (Number(point) === Number(newEstimatePoint)) isValid = true; + if (Number(point) === Number(newEstimatePoint)) isRepeated = true; }); } } else { if (estimateType === EEstimateSystem.CATEGORIES) { currentEstimatePoints.map((point) => { - if (point === newEstimatePoint.trim()) isValid = true; + if (point === newEstimatePoint.trim()) isRepeated = true; }); } else if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType)) { currentEstimatePoints.map((point) => { - if (Number(point) === Number(newEstimatePoint.trim())) isValid = true; + if (Number(point) === Number(newEstimatePoint.trim())) isRepeated = true; }); } } - return isValid; + return isRepeated; }; diff --git a/web/package.json b/web/package.json index c958e9aef..d4e7da7db 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^2.0.3", + "@headlessui/react": "^2.0.4", "@nivo/bar": "0.80.0", "@nivo/calendar": "0.80.0", "@nivo/core": "0.80.0", diff --git a/yarn.lock b/yarn.lock index d5e22a31d..8716189aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1600,7 +1600,7 @@ "@tanstack/react-virtual" "^3.0.0-beta.60" client-only "^0.0.1" -"@headlessui/react@^2.0.3": +"@headlessui/react@^2.0.3", "@headlessui/react@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.0.4.tgz#46cb39ca9dde3c2d15f4706c81dad78405b608f0" integrity sha512-16d/rOLeYsFsmPlRmXGu8DCBzrWD0zV1Ccx3n73wN87yFu8Y9+X04zflv8EJEt9TAYRyLKOmQXUnOnqQl6NgpA==