diff --git a/apps/app/components/estimates/create-update-estimate-modal.tsx b/apps/app/components/estimates/create-update-estimate-modal.tsx index 92c32a1c2..3d89e5fcf 100644 --- a/apps/app/components/estimates/create-update-estimate-modal.tsx +++ b/apps/app/components/estimates/create-update-estimate-modal.tsx @@ -6,19 +6,20 @@ import { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; // services import estimatesService from "services/estimates.service"; -// ui -import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -import { Dialog, Transition } from "@headlessui/react"; - // hooks import useToast from "hooks/use-toast"; - +// ui +import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; +// helpers +import { checkDuplicates } from "helpers/array.helper"; // types -import { IEstimate } from "types"; +import { IEstimate, IEstimateFormData } from "types"; // fetch-keys -import { ESTIMATES_LIST } from "constants/fetch-keys"; +import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys"; type Props = { isOpen: boolean; @@ -26,18 +27,35 @@ type Props = { data?: IEstimate; }; -const defaultValues: Partial = { +type FormValues = { + name: string; + description: string; + value1: string; + value2: string; + value3: string; + value4: string; + value5: string; + value6: string; +}; + +const defaultValues: Partial = { name: "", description: "", + value1: "", + value2: "", + value3: "", + value4: "", + value5: "", + value6: "", }; export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen }) => { const { register, - formState: { errors, isSubmitting }, + formState: { isSubmitting }, handleSubmit, reset, - } = useForm({ + } = useForm({ defaultValues, }); @@ -51,47 +69,48 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, const { setToastAlert } = useToast(); - const createEstimate = async (formData: IEstimate) => { + const createEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId) return; - const payload = { - name: formData.name, - description: formData.description, - }; - await estimatesService .createEstimate(workspaceSlug as string, projectId as string, payload) - .then((res) => { - mutate( - ESTIMATES_LIST(projectId as string), - (prevData) => [res, ...(prevData ?? [])], - false - ); + .then(() => { + mutate(ESTIMATES_LIST(projectId as string)); + onClose(); }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Error: Estimate could not be created", - }); + .catch((err) => { + if (err.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate with that name already exists. Please try again with another name.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate could not be created. Please try again.", + }); }); - - onClose(); }; - const updateEstimate = async (formData: IEstimate) => { + const updateEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId || !data) return; - const payload = { - name: formData.name, - description: formData.description, - }; - mutate( - ESTIMATES_LIST(projectId as string), + ESTIMATES_LIST(projectId.toString()), (prevData) => prevData?.map((p) => { - if (p.id === data.id) return { ...p, ...payload }; + if (p.id === data.id) + return { + ...p, + name: payload.estimate.name, + description: payload.estimate.description, + points: p.points.map((point, index) => ({ + ...point, + value: payload.estimate_points[index].value, + })), + }; return p; }), @@ -100,23 +119,116 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, await estimatesService .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload) - .then(() => handleClose()) + .then(() => { + mutate(ESTIMATES_LIST(projectId.toString())); + mutate(ESTIMATE_DETAILS(data.id)); + handleClose(); + }) .catch(() => { setToastAlert({ type: "error", title: "Error!", - message: "Error: Estimate could not be updated", + message: "Estimate could not be updated. Please try again.", }); }); onClose(); }; + const onSubmit = async (formData: FormValues) => { + if (!formData.name || formData.name === "") { + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate title cannot be empty.", + }); + return; + } + + if ( + formData.value1 === "" || + formData.value2 === "" || + formData.value3 === "" || + formData.value4 === "" || + formData.value5 === "" || + formData.value6 === "" + ) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate point cannot be empty.", + }); + return; + } + + if ( + checkDuplicates([ + formData.value1, + formData.value2, + formData.value3, + formData.value4, + formData.value5, + formData.value6, + ]) + ) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate points cannot have duplicate values.", + }); + return; + } + + const payload: IEstimateFormData = { + estimate: { + name: formData.name, + description: formData.description, + }, + estimate_points: [ + { + key: 0, + value: formData.value1, + }, + { + key: 1, + value: formData.value2, + }, + { + key: 2, + value: formData.value3, + }, + { + key: 3, + value: formData.value4, + }, + { + key: 4, + value: formData.value5, + }, + { + key: 5, + value: formData.value6, + }, + ], + }; + + if (data) await updateEstimate(payload); + else await createEstimate(payload); + }; + useEffect(() => { - reset({ - ...defaultValues, - ...data, - }); + if (data) + reset({ + ...defaultValues, + ...data, + value1: data.points[0]?.value, + value2: data.points[1]?.value, + value3: data.points[2]?.value, + value4: data.points[3]?.value, + value5: data.points[4]?.value, + value6: data.points[5]?.value, + }); + else reset({ ...defaultValues }); }, [data, reset]); return ( @@ -146,10 +258,8 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - -
+ +
{data ? "Update" : "Create"} Estimate @@ -161,17 +271,8 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, type="name" placeholder="Title" autoComplete="off" - mode="transparent" className="resize-none text-xl" - error={errors.name} register={register} - validations={{ - required: "Title is required", - maxLength: { - value: 255, - message: "Title should be less than 255 characters", - }, - }} />
@@ -180,11 +281,107 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, name="description" placeholder="Description" className="h-32 resize-none text-sm" - mode="transparent" - error={errors.description} register={register} />
+
+
+ + 1 + + + + +
+
+ + 2 + + + + +
+
+ + 3 + + + + +
+
+ + 4 + + + + +
+
+ + 5 + + + + +
+
+ + 6 + + + + +
+
Cancel diff --git a/apps/app/components/estimates/estimate-points-modal.tsx b/apps/app/components/estimates/estimate-points-modal.tsx deleted file mode 100644 index e3ce73aee..000000000 --- a/apps/app/components/estimates/estimate-points-modal.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useEffect } from "react"; - -import { useRouter } from "next/router"; - -import { useForm } from "react-hook-form"; - -import { mutate } from "swr"; - -// services -import estimatesService from "services/estimates.service"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { Input, PrimaryButton, SecondaryButton } from "components/ui"; -// types -import type { IEstimate, IEstimatePoint } from "types"; -// fetch-keys -import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; - -type Props = { - isOpen: boolean; - data?: IEstimatePoint[]; - estimate: IEstimate | null; - onClose: () => void; -}; - -interface FormValues { - value1: string; - value2: string; - value3: string; - value4: string; - value5: string; - value6: string; -} - -const defaultValues: FormValues = { - value1: "", - value2: "", - value3: "", - value4: "", - value5: "", - value6: "", -}; - -export const EstimatePointsModal: React.FC = ({ isOpen, data, estimate, onClose }) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const { - register, - formState: { isSubmitting }, - handleSubmit, - reset, - } = useForm({ defaultValues }); - - const handleClose = () => { - onClose(); - reset(); - }; - - const createEstimatePoints = async (formData: FormValues) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - estimate_points: [ - { - key: 0, - value: formData.value1, - }, - { - key: 1, - value: formData.value2, - }, - { - key: 2, - value: formData.value3, - }, - { - key: 3, - value: formData.value4, - }, - { - key: 4, - value: formData.value5, - }, - { - key: 5, - value: formData.value6, - }, - ], - }; - - await estimatesService - .createEstimatePoints( - workspaceSlug as string, - projectId as string, - estimate?.id as string, - payload - ) - .then(() => { - handleClose(); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate points could not be created. Please try again.", - }); - }); - }; - - const updateEstimatePoints = async (formData: FormValues) => { - if (!workspaceSlug || !projectId || !data || data.length === 0) return; - - const payload = { - estimate_points: data.map((d, index) => ({ - id: d.id, - value: (formData as any)[`value${index + 1}`], - })), - }; - - await estimatesService - .patchEstimatePoints( - workspaceSlug as string, - projectId as string, - estimate?.id as string, - payload - ) - .then(() => { - handleClose(); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate points could not be created. Please try again.", - }); - }); - }; - - const onSubmit = async (formData: FormValues) => { - let c = 0; - - Object.keys(formData).map((key) => { - if (formData[key as keyof FormValues] === "") c++; - }); - - if (c !== 0) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Please fill all the fields.", - }); - return; - } - - if (data && data.length !== 0) await updateEstimatePoints(formData); - else await createEstimatePoints(formData); - - if (estimate) mutate(ESTIMATE_POINTS_LIST(estimate.id)); - }; - - useEffect(() => { - if (!data || data.length < 6) return; - - reset({ - ...defaultValues, - value1: data[0].value, - value2: data[1].value, - value3: data[2].value, - value4: data[3].value, - value5: data[4].value, - value6: data[5].value, - }); - }, [data, reset]); - - return ( - - handleClose()}> - -
- - -
-
- - - -
-
-

- {data && data.length > 0 ? "Update" : "Create"} Estimate Points -

-
-
- - 1 - - - - -
-
- - 2 - - - - -
-
- - 3 - - - - -
-
- - 4 - - - - -
-
- - 5 - - - - -
-
- - 6 - - - - -
-
-
-
- -
- handleClose()}>Cancel - - {data && data.length > 0 - ? isSubmitting - ? "Updating Points..." - : "Update Points" - : isSubmitting - ? "Creating Points..." - : "Create Points"} - -
- -
-
-
-
-
-
- ); -}; diff --git a/apps/app/components/estimates/index.tsx b/apps/app/components/estimates/index.tsx index 40248b525..f20c74780 100644 --- a/apps/app/components/estimates/index.tsx +++ b/apps/app/components/estimates/index.tsx @@ -1,4 +1,3 @@ export * from "./create-update-estimate-modal"; export * from "./single-estimate"; -export * from "./estimate-points-modal" -export * from "./delete-estimate-modal" +export * from "./delete-estimate-modal"; diff --git a/apps/app/components/estimates/single-estimate.tsx b/apps/app/components/estimates/single-estimate.tsx index e0155675b..43dcc45b7 100644 --- a/apps/app/components/estimates/single-estimate.tsx +++ b/apps/app/components/estimates/single-estimate.tsx @@ -2,31 +2,21 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; - // services -import estimatesService from "services/estimates.service"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; import useProjectDetails from "hooks/use-project-details"; // components -import { EstimatePointsModal, DeleteEstimateModal } from "components/estimates"; +import { DeleteEstimateModal } from "components/estimates"; // ui -import { CustomMenu } from "components/ui"; +import { CustomMenu, SecondaryButton } from "components/ui"; //icons -import { - PencilIcon, - TrashIcon, - SquaresPlusIcon, - ListBulletIcon, -} from "@heroicons/react/24/outline"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types import { IEstimate } from "types"; -// fetch-keys -import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; type Props = { estimate: IEstimate; @@ -39,7 +29,6 @@ export const SingleEstimate: React.FC = ({ editEstimate, handleEstimateDelete, }) => { - const [isEstimatePointsModalOpen, setIsEstimatePointsModalOpen] = useState(false); const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false); const router = useRouter(); @@ -49,18 +38,6 @@ export const SingleEstimate: React.FC = ({ const { projectDetails, mutateProjectDetails } = useProjectDetails(); - const { data: estimatePoints } = useSWR( - workspaceSlug && projectId ? ESTIMATE_POINTS_LIST(estimate.id) : null, - workspaceSlug && projectId - ? () => - estimatesService.getEstimatesPointsList( - workspaceSlug as string, - projectId as string, - estimate.id - ) - : null - ); - const handleUseEstimate = async () => { if (!workspaceSlug || !projectId) return; @@ -87,78 +64,61 @@ export const SingleEstimate: React.FC = ({ return ( <> - setIsEstimatePointsModalOpen(false)} - data={estimatePoints ? orderArrayBy(estimatePoints, "key") : undefined} - />
-
+
-
+
{estimate.name} {projectDetails?.estimate && projectDetails?.estimate === estimate.id && ( - + In use )}
-

+

{estimate.description}

- - {projectDetails?.estimate !== estimate.id && - estimatePoints && - estimatePoints.length > 0 && ( - -
- - Use estimate -
-
- )} - setIsEstimatePointsModalOpen(true)}> -
- - - {estimatePoints && estimatePoints?.length > 0 ? "Edit points" : "Create points"} - -
-
- { - editEstimate(estimate); - }} - > -
- - Edit estimate -
-
- {projectDetails?.estimate !== estimate.id && ( +
+ {projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && ( + + Use + + )} + { - setIsDeleteEstimateModalOpen(true); + editEstimate(estimate); }} >
- - Delete estimate + + Edit estimate
- )} -
+ {projectDetails?.estimate !== estimate.id && ( + { + setIsDeleteEstimateModalOpen(true); + }} + > +
+ + Delete estimate +
+
+ )} + +
- {estimatePoints && estimatePoints.length > 0 ? ( -
+ {estimate.points.length > 0 ? ( +
Estimate points ( - {estimatePoints.map((point, index) => ( -
+ {orderArrayBy(estimate.points, "key").map((point, index) => ( +
{point.value} - {index !== estimatePoints.length - 1 && ","}{" "} + {index !== estimate.points.length - 1 && ","}{" "}
))}
@@ -166,7 +126,7 @@ export const SingleEstimate: React.FC = ({
) : (
-

No estimate points

+

No estimate points

)}
diff --git a/apps/app/components/issues/select/estimate.tsx b/apps/app/components/issues/select/estimate.tsx index 813bd14f0..142a0369b 100644 --- a/apps/app/components/issues/select/estimate.tsx +++ b/apps/app/components/issues/select/estimate.tsx @@ -22,7 +22,7 @@ export const IssueEstimateSelect: React.FC = ({ value, onChange }) => { value={value} label={
- + {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"} diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index a44c01ce0..737896c4f 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -56,7 +56,7 @@ export const ViewEstimateSelect: React.FC = ({ }} label={ -
+
{estimateValue ?? "Estimate"}
diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 54bd85eee..3a13faf25 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -98,8 +98,7 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select setToastAlert({ type: "error", title: "Error!", - message: - "Another state exists with the same name. Please try again with another name.", + message: "State with that name already exists. Please try again with another name.", }); else setToastAlert({ diff --git a/apps/app/components/ui/input/index.tsx b/apps/app/components/ui/input/index.tsx index 0139dae1a..b2c93193a 100644 --- a/apps/app/components/ui/input/index.tsx +++ b/apps/app/components/ui/input/index.tsx @@ -1,7 +1,7 @@ import * as React from "react"; -// common -import { Props } from "./types"; + // types +import { Props } from "./types"; export const Input: React.FC = ({ label, @@ -21,7 +21,7 @@ export const Input: React.FC = ({ }) => ( <> {label && ( -