[WEB-522] chore: estimate restructure and handled error while creating estimates (#4817)

* dev:updated estimate UI

* dev: updated error messages in estimate point create
This commit is contained in:
guru_sainath 2024-06-14 13:23:58 +05:30 committed by GitHub
parent 9145234a6c
commit b3626d815f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 73 deletions

View File

@ -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<number, TEstimateTypeErrorObject>
| undefined;

View File

@ -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<TCreateEstimateModal> = observer((props) =>
// states
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.POINTS);
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
const [estimatePointCreate, setEstimatePointCreate] = useState<TEstimatePointsObject[] | undefined>(undefined);
const [estimatePointCreateError, setEstimatePointCreateError] = useState<number[]>([]);
const [estimatePointError, setEstimatePointError] = useState<TEstimateTypeError>(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<TCreateEstimateModal> = 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<TCreateEstimateModal> = 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 (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
@ -122,20 +175,16 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = 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 && (
<div className="pt-5 text-sm text-red-500">
Estimate points can&apos;t be empty. Enter a value in each field or remove those you don&apos;t have
values for.
</div>
)}
)} */}
</div>
<div className="relative flex justify-end items-center gap-3 px-5 pt-5 border-t border-custom-border-200">

View File

@ -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<SetStateAction<TEstimatePointsObject[] | undefined>>;
estimatePointCreate: TEstimatePointsObject[] | undefined;
setEstimatePointCreate: Dispatch<SetStateAction<TEstimatePointsObject[] | undefined>>;
estimatePointCreateError: number[];
estimatePointError?: TEstimateTypeError;
handleEstimatePointError?: (
key: number,
oldValue: string,
newValue: string,
message: string | undefined,
mode: "add" | "delete"
) => void;
};
export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = observer((props) => {
@ -31,10 +36,11 @@ export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = observer((p
estimateType,
estimatePoints,
setEstimatePoints,
estimatePointCreate,
setEstimatePointCreate,
estimatePointCreateError,
estimatePointError,
handleEstimatePointError,
} = props;
// states
const [estimatePointCreate, setEstimatePointCreate] = useState<TEstimatePointsObject[] | undefined>(undefined);
const handleEstimatePoint = useCallback(
(mode: "add" | "remove" | "update", value: TEstimatePointsObject) => {
@ -90,11 +96,13 @@ export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = 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<TEstimatePointCreateRoot> = 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<TEstimatePointCreateRoot> = 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 && (

View File

@ -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<TEstimatePointCreate> = observer((props) => {
@ -35,21 +36,14 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = 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<string | undefined>(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<TEstimatePointCreate> = 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<HTMLFormElement>) => {
@ -72,7 +67,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = 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<TEstimatePointCreate> = 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<TEstimatePointCreate> = 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<TEstimatePointCreate> = 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<TEstimatePointCreate> = 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 (
<form onSubmit={handleCreate} className="relative flex items-center gap-2 text-base">
<form onSubmit={handleCreate} className="relative flex items-center gap-2 text-base pr-2.5">
<div
className={cn(
"relative w-full border rounded flex items-center my-1",
error ? `border-red-500` : `border-custom-border-200`
estimatePointError?.message ? `border-red-500` : `border-custom-border-200`
)}
>
<input
@ -158,8 +159,8 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
placeholder="Enter estimate point"
autoFocus
/>
{error && (
<Tooltip tooltipContent={error} position="bottom">
{estimatePointError?.message && (
<Tooltip tooltipContent={estimatePointError?.message} position="bottom">
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden mr-3 relative flex justify-center items-center text-red-500">
<Info size={14} />
</div>

View File

@ -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<TEstimatePointItemPreview> = observer((props) => {
@ -30,6 +32,8 @@ export const EstimatePointItemPreview: FC<TEstimatePointItemPreview> = observer(
estimatePoints,
handleEstimatePointValueUpdate,
handleEstimatePointValueRemove,
estimatePointError,
handleEstimatePointError,
} = props;
// state
const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false);
@ -90,6 +94,8 @@ export const EstimatePointItemPreview: FC<TEstimatePointItemPreview> = observer(
handleEstimatePointValueUpdate && handleEstimatePointValueUpdate(estimatePointValue)
}
closeCallBack={() => setEstimatePointEditToggle(false)}
estimatePointError={estimatePointError}
handleEstimatePointError={handleEstimatePointError}
/>
)}

View File

@ -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<TEstimatePointUpdate> = observer((props) => {
@ -36,13 +38,14 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
estimatePoint,
handleEstimatePointValueUpdate,
closeCallBack,
estimatePointError,
handleEstimatePointError,
} = props;
// hooks
const { updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId);
// states
const [loader, setLoader] = useState(false);
const [estimateInputValue, setEstimateInputValue] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
useEffect(() => {
if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || "");
@ -60,7 +63,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
};
const handleEstimateInputValue = (value: string) => {
setError(undefined);
handleEstimatePointError && handleEstimatePointError(value, undefined);
setEstimateInputValue(() => value);
};
@ -69,7 +72,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = 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 (
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 text-base">
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 text-base pr-2.5">
<div
className={cn(
"relative w-full border rounded flex items-center my-1",
error ? `border-red-500` : `border-custom-border-200`
estimatePointError?.message ? `border-red-500` : `border-custom-border-200`
)}
>
<input
@ -156,9 +167,9 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
placeholder="Enter estimate point"
autoFocus
/>
{error && (
{estimatePointError?.message && (
<>
<Tooltip tooltipContent={error} position="bottom">
<Tooltip tooltipContent={estimatePointError?.message} position="bottom">
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden mr-3 relative flex justify-center items-center text-red-500">
<Info size={14} />
</div>