chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point

This commit is contained in:
guru_sainath 2024-05-29 17:19:14 +05:30
parent 18c5b2a0a6
commit c2e07c6b7c
9 changed files with 235 additions and 87 deletions

View File

@ -62,7 +62,6 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = 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<TCreateEstimateModal> = observer((props) =>
});
handleClose();
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Estimate point values cannot be repeated",
});
}
} else {
setToast({

View File

@ -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<TEstimatePointCreate> = 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,6 +51,15 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
}
}
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,
@ -63,6 +73,14 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
setLoader(false);
setError("please enter a valid estimate value");
}
} else {
setLoader(false);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Estimate point values cannot be repeated",
});
}
} catch {
setLoader(false);
setError("something went wrong. please try again later");

View File

@ -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,12 +31,18 @@ export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) =>
callback();
};
const handleCreate = async () => {
const handleDelete = async () => {
if (!workspaceSlug || !projectId || !projectId) return;
if (estimateInputValue)
try {
setLoader(true);
setError(undefined);
await deleteEstimatePoint(workspaceSlug, projectId, estimatePointId, estimateInputValue);
await deleteEstimatePoint(
workspaceSlug,
projectId,
estimatePointId,
estimateInputValue === "none" ? undefined : estimateInputValue
);
setLoader(false);
setError(undefined);
handleClose();
@ -44,15 +50,18 @@ export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) =>
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 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 (
<div className="relative flex items-center gap-2 text-base">
@ -60,37 +69,17 @@ export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) =>
<div className="w-full border border-custom-border-200 rounded p-2.5 bg-custom-background-90">
{estimatePoint?.value}
</div>
<div className="relative flex justify-center items-center gap-2 whitespace-nowrap">
<div className="text-sm first-letter:relative flex justify-center items-center gap-2 whitespace-nowrap">
Mark as <MoveRight size={14} />
</div>
<div
className={cn(
"relative w-full rounded border flex items-center gap-3 p-2.5",
error ? `border-red-500` : `border-custom-border-200`
)}
>
<Select
className="bg-transparent flex-grow focus:ring-0 focus:border-0 focus:outline-none"
value={estimateInputValue}
onChange={(e) => setEstimateInputValue(e.target.value)}
>
<option value={undefined}>None</option>
{selectDropdownOptions.map((option) => (
<option key={option?.id} value={option?.value}>
{option?.value}
</option>
))}
</Select>
{error && (
<>
<Tooltip tooltipContent={error} position="bottom">
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden relative flex justify-center items-center text-red-500">
<Info size={14} />
</div>
</Tooltip>
</>
)}
</div>
<EstimatePointDropdown
options={selectDropdownOptions}
error={error}
callback={(estimateId: string) => {
setEstimateInputValue(estimateId);
setError(undefined);
}}
/>
</div>
{loader ? (
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
@ -99,7 +88,7 @@ export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) =>
) : (
<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 text-red-500"
onClick={handleCreate}
onClick={handleDelete}
>
<Trash2 size={14} />
</div>

View File

@ -3,3 +3,4 @@ export * from "./preview";
export * from "./create";
export * from "./update";
export * from "./delete";
export * from "./select-dropdown";

View File

@ -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<TEstimatePointDropdown> = (props) => {
const { options, error, callback } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | undefined>(undefined);
// ref
const dropdownContainerRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useDynamicDropdownPosition(isDropdownOpen, () => setIsDropdownOpen(false), buttonRef, dropdownRef);
useOutsideClickDetector(dropdownContainerRef, () => setIsDropdownOpen(false));
// derived values
const selectedValue = options.find((option) => option?.id === selectedOption) || undefined;
return (
<div ref={dropdownContainerRef} className="w-full relative">
<Listbox
as="div"
value={selectedOption}
onChange={(selectedOption) => {
setSelectedOption(selectedOption);
callback(selectedOption);
setIsDropdownOpen(false);
}}
className="w-full flex-shrink-0 text-left"
>
<ListboxButton
type="button"
ref={buttonRef}
onClick={() => 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`
)}
>
<div
className={cn(`w-full text-sm text-left`, !selectedValue ? "text-custom-text-300" : "text-custom-text-100")}
>
{selectedValue?.value || "Select an estimate point"}
</div>
<ChevronDown className={`size-3 ${true ? "stroke-onboarding-text-400" : "stroke-onboarding-text-100"}`} />
{error && (
<>
<Tooltip tooltipContent={error} position="bottom">
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden relative flex justify-center items-center text-red-500">
<Info size={14} />
</div>
</Tooltip>
</>
)}
</ListboxButton>
<Transition
show={isDropdownOpen}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<ListboxOptions
ref={dropdownRef}
className="fixed z-10 mt-1 h-fit w-48 sm:w-60 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 shadow-sm focus:outline-none"
>
<div className="p-1.5">
<ListboxOption
value={"none"}
className={cn(
`cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`,
selectedOption === "none" ? "text-custom-text-100" : "text-custom-text-300"
)}
>
<div className="relative flex items-center text-wrap gap-2 px-1 py-0.5">
<div className="text-sm font-medium w-full line-clamp-1">None</div>
{selectedOption === "none" && <Check size={12} />}
</div>
</ListboxOption>
{options.map((option) => (
<ListboxOption
key={option?.key}
value={option?.id}
className={cn(
`cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`,
selectedOption === option?.id ? "text-custom-text-100" : "text-custom-text-300"
)}
>
<div className="relative flex items-center text-wrap gap-2 px-1 py-0.5">
<div className="text-sm font-medium w-full line-clamp-1">{option.value}</div>
{selectedOption === option?.id && <Check size={12} />}
</div>
</ListboxOption>
))}
</div>
</ListboxOptions>
</Transition>
</Listbox>
</div>
);
};

View File

@ -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<TEstimatePointUpdate> = 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<TEstimatePointUpdate> = observer((props) =>
callback();
};
const handleCreate = async () => {
const handleUpdate = async () => {
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
if (estimateInputValue)
try {
@ -56,6 +57,15 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
}
}
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,
@ -68,6 +78,14 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
setLoader(false);
setError("please enter a valid estimate value");
}
} else {
setLoader(false);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Estimate point values cannot be repeated",
});
}
} catch {
setLoader(false);
setError("something went wrong. please try again later");
@ -110,7 +128,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
) : (
<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 text-green-500"
onClick={handleCreate}
onClick={handleUpdate}
>
<Check size={14} />
</div>

View File

@ -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;
};

View File

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

View File

@ -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==