mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point
This commit is contained in:
parent
18c5b2a0a6
commit
c2e07c6b7c
@ -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({
|
||||
|
@ -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,18 +51,35 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = 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);
|
||||
|
@ -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<TEstimatePointDelete> = 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 (
|
||||
<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>
|
||||
|
@ -3,3 +3,4 @@ export * from "./preview";
|
||||
export * from "./create";
|
||||
export * from "./update";
|
||||
export * from "./delete";
|
||||
export * from "./select-dropdown";
|
||||
|
119
web/components/estimates/points/edit/select-dropdown.tsx
Normal file
119
web/components/estimates/points/edit/select-dropdown.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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,17 +57,34 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = 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<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>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user