chore: updated modal and form validations

This commit is contained in:
gurusainath 2024-06-10 17:11:34 +05:30
parent d3556f457b
commit b302addcd9
9 changed files with 176 additions and 134 deletions

View File

@ -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> = (props) => {
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"

View File

@ -28,6 +28,8 @@ 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 [buttonLoader, setButtonLoader] = useState(false);
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
@ -40,33 +42,39 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = 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<TCreateEstimateModal> = observer((props) =>
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">
{/* heading */}
<div className="relative flex justify-between items-center gap-2 px-5">
@ -90,7 +98,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
<ChevronLeft className="w-4 h-4" />
</div>
)}
<div className="text-xl font-medium text-custom-text-100">New Estimate System</div>
<div className="text-xl font-medium text-custom-text-100">New estimate system</div>
</div>
<div className="text-xs text-gray-400">Step {renderEstimateStepsCount} of 2</div>
</div>
@ -107,16 +115,26 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
/>
)}
{estimatePoints && (
<>
<EstimatePointCreateRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={undefined}
estimateType={estimateSystem}
estimatePoints={estimatePoints}
setEstimatePoints={setEstimatePoints}
/>
</>
<EstimatePointCreateRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={undefined}
estimateType={estimateSystem}
estimatePoints={estimatePoints}
setEstimatePoints={setEstimatePoints}
estimatePointCreate={estimatePointCreate}
setEstimatePointCreate={(value) => {
setEstimatePointCreateError([]);
setEstimatePointCreate(value);
}}
estimatePointCreateError={estimatePointCreateError}
/>
)}
{estimatePointCreateError.length > 0 && (
<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>

View File

@ -53,7 +53,7 @@ export const DeleteEstimateModal: FC<TDeleteEstimateModal> = observer((props) =>
};
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">
{/* heading */}
<div className="relative flex justify-between items-center gap-2 px-5">

View File

@ -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<SetStateAction<TEstimatePointsObject[] | undefined>>;
estimatePointCreate: TEstimatePointsObject[] | undefined;
setEstimatePointCreate: Dispatch<SetStateAction<TEstimatePointsObject[] | undefined>>;
estimatePointCreateError: number[];
};
export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = observer((props) => {
// props
const { workspaceSlug, projectId, estimateId, estimateType, estimatePoints, setEstimatePoints } = props;
// states
const [estimatePointCreate, setEstimatePointCreate] = useState<TEstimatePointsObject[] | undefined>(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<TEstimatePointCreateRoot> = 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 (
<div className="space-y-3">
<div className="space-y-1">
<div className="text-sm font-medium text-custom-text-200 capitalize">{estimateType}</div>
<div>
@ -118,21 +139,12 @@ export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = 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 && (
<Button
variant="link-primary"
size="sm"
prependIcon={<Plus />}
onClick={() =>
handleEstimatePointCreate("add", {
id: undefined,
key: estimatePoints.length + (estimatePointCreate?.length || 0) + 1,
value: "",
})
}
>
{estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= maxEstimatesCount - 1 && (
<Button variant="link-primary" size="sm" prependIcon={<Plus />} onClick={handleCreate}>
Add {estimateType}
</Button>
)}

View File

@ -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<TEstimatePointCreate> = observer((props) => {
@ -32,6 +34,8 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
estimatePoints,
handleEstimatePointValue,
closeCallBack,
handleCreateCallback,
isError,
} = props;
// hooks
const { creteEstimatePoint } = useEstimate(estimateId);
@ -40,6 +44,13 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
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);
setEstimateInputValue("");
@ -51,7 +62,12 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
closeCallBack();
};
const handleCreate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
const handleEstimateInputValue = (value: string) => {
setError(undefined);
setEstimateInputValue(value);
};
const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!workspaceSlug || !projectId) return;
@ -110,6 +126,9 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
}
} else {
handleSuccess(estimateInputValue);
if (handleCreateCallback) {
handleCreateCallback();
}
}
} else {
setLoader(false);
@ -124,7 +143,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
};
return (
<form className="relative flex items-center gap-2 text-base">
<form onSubmit={handleCreate} className="relative flex items-center gap-2 text-base">
<div
className={cn(
"relative w-full border rounded flex items-center my-1",
@ -134,42 +153,37 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
<input
type="text"
value={estimateInputValue}
onChange={(e) => 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 && (
<>
<Tooltip tooltipContent={error} 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>
</Tooltip>
</>
<Tooltip tooltipContent={error} 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>
</Tooltip>
)}
</div>
{estimateId && (
<>
<button
type="submit"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
disabled={loader}
onClick={handleCreate}
>
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
</button>
<button
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={handleClose}
disabled={loader}
>
<X size={14} className="text-custom-text-200" />
</button>
</>
{estimateInputValue && estimateInputValue.length > 0 && (
<button
type="submit"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
disabled={loader}
>
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
</button>
)}
<button
type="button"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={handleClose}
disabled={loader}
>
<X size={14} className="text-custom-text-200" />
</button>
</form>
);
});

View File

@ -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<TEstimatePointItemPreview> = observer(
>
<Pencil size={14} className="text-custom-text-200" />
</div>
<div
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={() =>
estimateId && estimatePointId
? setEstimatePointDeleteToggle(true)
: handleEstimatePointValueRemove && handleEstimatePointValueRemove()
}
>
<Trash2 size={14} className="text-custom-text-200" />
</div>
{estimatePoints.length > minEstimatesCount && (
<div
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={() =>
estimateId && estimatePointId
? setEstimatePointDeleteToggle(true)
: handleEstimatePointValueRemove && handleEstimatePointValueRemove()
}
>
<Trash2 size={14} className="text-custom-text-200" />
</div>
)}
</div>
)}

View File

@ -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<TEstimatePointUpdate> = observer((props) =>
closeCallBack();
};
const handleUpdate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
const handleEstimateInputValue = (value: string) => {
setError(undefined);
setEstimateInputValue(() => value);
};
const handleUpdate = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!workspaceSlug || !projectId) return;
@ -71,7 +76,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = observer((props) =>
};
return (
<form className="relative flex items-center gap-2 text-base">
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 text-base">
<div
className={cn(
"relative w-full border rounded flex items-center my-1",
@ -146,10 +151,9 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
<input
type="text"
value={estimateInputValue}
onChange={(e) => 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<TEstimatePointUpdate> = observer((props) =>
</>
)}
</div>
{estimateId && (
<>
<button
type="submit"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
disabled={loader}
onClick={handleUpdate}
>
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
</button>
<button
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={handleClose}
disabled={loader}
>
<X size={14} className="text-custom-text-200" />
</button>
</>
{estimateInputValue && estimateInputValue.length > 0 && (
<button
type="submit"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
disabled={loader}
>
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
</button>
)}
<button
type="button"
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
onClick={handleClose}
disabled={loader}
>
<X size={14} className="text-custom-text-200" />
</button>
</form>
);
});

View File

@ -20,7 +20,7 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
const { isOpen, handleClose } = props;
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">
{/* heading */}
<div className="relative flex justify-between items-center gap-2 px-5">

View File

@ -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" },
],
},
},