mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: updated ceate estimare workflow
This commit is contained in:
parent
87d317ab82
commit
39482b72ab
@ -5,9 +5,12 @@ import { cn } from "../../helpers";
|
||||
|
||||
type RadioInputProps = {
|
||||
label: string | React.ReactNode | undefined;
|
||||
wrapperClassName?: string;
|
||||
fieldClassName?: string;
|
||||
buttonClassName?: string;
|
||||
labelClassName?: string;
|
||||
ariaLabel?: string;
|
||||
options: { label: string; value: string; disabled?: boolean }[];
|
||||
options: { label: string | React.ReactNode; value: string; disabled?: boolean }[];
|
||||
vertical?: boolean;
|
||||
selected: string;
|
||||
onChange: (value: string) => void;
|
||||
@ -16,7 +19,10 @@ type RadioInputProps = {
|
||||
|
||||
export const RadioInput = ({
|
||||
label: inputLabel,
|
||||
labelClassName: inputLabelClassName,
|
||||
labelClassName: inputLabelClassName = "",
|
||||
wrapperClassName: inputWrapperClassName = "",
|
||||
fieldClassName: inputFieldClassName = "",
|
||||
buttonClassName: inputButtonClassName = "",
|
||||
options,
|
||||
vertical,
|
||||
selected,
|
||||
@ -42,15 +48,15 @@ export const RadioInput = ({
|
||||
return (
|
||||
<RadioGroup value={selected} onChange={setSelected} aria-label={aria} className={className}>
|
||||
<Label className={cn(`mb-2`, inputLabelClassName)}>{inputLabel}</Label>
|
||||
<div className={`${wrapperClass}`}>
|
||||
{options.map(({ value, label, disabled }) => (
|
||||
<Field key={label} className="flex items-center gap-2">
|
||||
<div className={cn(`${wrapperClass}`, inputWrapperClassName)}>
|
||||
{options.map(({ value, label, disabled }, index) => (
|
||||
<Field key={index} className={cn("flex items-center gap-2", inputFieldClassName)}>
|
||||
<Radio
|
||||
value={value}
|
||||
className="group flex size-5 items-center justify-center rounded-full border border-custom-border-400 bg-custom-background-500 data-[checked]:bg-custom-primary-200 data-[checked]:border-custom-primary-100 cursor-pointer
|
||||
data-[disabled]:bg-custom-background-200
|
||||
data-[disabled]:border-custom-border-200
|
||||
data-[disabled]:cursor-not-allowed"
|
||||
className={cn(
|
||||
"group flex size-5 items-center justify-center rounded-full border border-custom-border-400 bg-custom-background-500 data-[checked]:bg-custom-primary-200 data-[checked]:border-custom-primary-100 cursor-pointer data-[disabled]:bg-custom-background-200 data-[disabled]:border-custom-border-200 data-[disabled]:cursor-not-allowed",
|
||||
inputButtonClassName
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from "./modal";
|
||||
export * from "./stage-one";
|
||||
export * from "./stage-two";
|
||||
|
@ -5,7 +5,7 @@ import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject } from "@
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/components/estimates";
|
||||
import { EstimateCreateStageOne, EstimatePointCreateRoot } from "@/components/estimates";
|
||||
// constants
|
||||
import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||
// hooks
|
||||
@ -36,9 +36,6 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// derived values
|
||||
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
|
||||
|
||||
const handleCreateEstimate = async () => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
@ -90,6 +87,9 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
||||
}
|
||||
};
|
||||
|
||||
// derived values
|
||||
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<div className="relative space-y-6 py-5">
|
||||
@ -107,7 +107,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xl font-medium text-custom-text-200 ">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}/2</div>
|
||||
</div>
|
||||
@ -124,12 +124,10 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
||||
/>
|
||||
)}
|
||||
{estimatePoints && (
|
||||
<EstimateCreateStageTwo
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateSystem={estimateSystem}
|
||||
<EstimatePointCreateRoot
|
||||
estimateType={estimateSystem}
|
||||
estimatePoints={estimatePoints}
|
||||
handleEstimatePoints={handleUpdatePoints}
|
||||
setEstimatePoints={setEstimatePoints}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -18,49 +18,51 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
||||
|
||||
if (!currentEstimateSystem) return <></>;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-7">
|
||||
<div className="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0 gap-2 mb-2">
|
||||
<RadioInput
|
||||
options={Object.keys(ESTIMATE_SYSTEMS).map((system) => {
|
||||
const currentSystem = system as TEstimateSystemKeys;
|
||||
return {
|
||||
label: ESTIMATE_SYSTEMS[currentSystem]?.name,
|
||||
label: ESTIMATE_SYSTEMS[currentSystem]?.name || <div>Hello</div>,
|
||||
value: system,
|
||||
disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available,
|
||||
};
|
||||
})}
|
||||
label="Choose an estimate system"
|
||||
labelClassName="text-sm font-medium text-custom-text-200 mb-3"
|
||||
wrapperClassName="relative flex flex-wrap gap-14"
|
||||
fieldClassName="relative flex items-center gap-2"
|
||||
buttonClassName="size-4"
|
||||
selected={estimateSystem}
|
||||
onChange={(value) => handleEstimateSystem(value as TEstimateSystemKeys)}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-custom-text-200">Start from scratch</div>
|
||||
<button
|
||||
className="border border-custom-border-200 rounded-md p-2 text-left flex-1 w-full block"
|
||||
className="border border-custom-border-200 rounded-md p-3 py-2.5 text-left space-y-1 w-full block hover:bg-custom-background-90"
|
||||
onClick={() => handleEstimatePoints("custom")}
|
||||
>
|
||||
<p className="block text-sm">Custom</p>
|
||||
<p className="block text-base">Custom</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Add your own <span className="lowercase">{currentEstimateSystem.name}</span> from scratch
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-custom-text-200">Choose a template</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
{Object.keys(currentEstimateSystem.templates).map((name) =>
|
||||
currentEstimateSystem.templates[name]?.hide ? null : (
|
||||
<button
|
||||
key={name}
|
||||
className="border border-custom-border-200 rounded-md p-2 text-left"
|
||||
className="border border-custom-border-200 rounded-md p-3 py-2.5 text-left space-y-1 hover:bg-custom-background-90"
|
||||
onClick={() => handleEstimatePoints(name)}
|
||||
>
|
||||
<p className="block text-sm">{currentEstimateSystem.templates[name]?.title}</p>
|
||||
<p className="block text-base">{currentEstimateSystem.templates[name]?.title}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{currentEstimateSystem.templates[name]?.values?.map((template) => template?.value)?.join(", ")}
|
||||
</p>
|
||||
|
@ -1,92 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { TEstimatePointsObject } from "@plane/types";
|
||||
import { Button, Sortable } from "@plane/ui";
|
||||
// components
|
||||
import { EstimatePointItem } from "@/components/estimates";
|
||||
// constants
|
||||
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS, maxEstimatesCount } from "@/constants/estimates";
|
||||
|
||||
type TEstimateCreateStageTwo = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
estimateSystem: EEstimateSystem;
|
||||
estimatePoints: TEstimatePointsObject[];
|
||||
handleEstimatePoints: (value: TEstimatePointsObject[]) => void;
|
||||
};
|
||||
|
||||
export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = observer((props) => {
|
||||
const { workspaceSlug, projectId, estimateSystem, estimatePoints, handleEstimatePoints } = props;
|
||||
|
||||
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined;
|
||||
|
||||
const addNewEstimationPoint = () => {
|
||||
const currentEstimationPoints = estimatePoints;
|
||||
const newEstimationPoint: TEstimatePointsObject = {
|
||||
key: currentEstimationPoints.length + 1,
|
||||
value: "",
|
||||
};
|
||||
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
|
||||
};
|
||||
|
||||
const editEstimationPoint = (index: number, value: string) => {
|
||||
const newEstimationPoints = estimatePoints;
|
||||
newEstimationPoints[index].value = value;
|
||||
handleEstimatePoints(newEstimationPoints);
|
||||
};
|
||||
|
||||
const deleteEstimationPoint = (index: number) => {
|
||||
let newEstimationPoints = estimatePoints;
|
||||
newEstimationPoints.splice(index, 1);
|
||||
newEstimationPoints = newEstimationPoints.map((item, index) => ({
|
||||
...item,
|
||||
key: index + 1,
|
||||
}));
|
||||
handleEstimatePoints(newEstimationPoints);
|
||||
};
|
||||
|
||||
const replaceEstimateItem = (index: number, value: TEstimatePointsObject) => {
|
||||
const newEstimationPoints = estimatePoints;
|
||||
newEstimationPoints[index] = value;
|
||||
handleEstimatePoints(newEstimationPoints);
|
||||
};
|
||||
|
||||
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) => {
|
||||
const sortedEstimatePoints = updatedEstimatePoints.map((item, index) => ({
|
||||
...item,
|
||||
key: index + 1,
|
||||
})) as TEstimatePointsObject[];
|
||||
return sortedEstimatePoints;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-custom-text-300">{estimateSystem}</div>
|
||||
<div className="space-y-3">
|
||||
<Sortable
|
||||
data={estimatePoints}
|
||||
render={(value: TEstimatePointsObject, index: number) => (
|
||||
<EstimatePointItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateId={undefined}
|
||||
mode={EEstimateUpdateStages.CREATE}
|
||||
item={value}
|
||||
editItem={(value: string) => editEstimationPoint(index, value)}
|
||||
replaceEstimateItem={(value: TEstimatePointsObject) => replaceEstimateItem(index, value)}
|
||||
deleteItem={() => deleteEstimationPoint(index)}
|
||||
/>
|
||||
)}
|
||||
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
|
||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||
/>
|
||||
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
|
||||
<Button size="sm" prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
||||
Add {currentEstimateSystem?.name}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
3
web/components/estimates/points/create/index.ts
Normal file
3
web/components/estimates/points/create/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./root";
|
||||
export * from "./preview";
|
||||
export * from "./update";
|
65
web/components/estimates/points/create/preview.tsx
Normal file
65
web/components/estimates/points/create/preview.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
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";
|
||||
// components
|
||||
import { EstimatePointItemCreateUpdate } from "@/components/estimates/points";
|
||||
|
||||
type TEstimatePointItemCreatePreview = {
|
||||
estimateType: TEstimateSystemKeys;
|
||||
estimatePoint: TEstimatePointsObject;
|
||||
handleEstimatePoint: (mode: "add" | "remove" | "update", value: TEstimatePointsObject) => void;
|
||||
};
|
||||
|
||||
export const EstimatePointItemCreatePreview: FC<TEstimatePointItemCreatePreview> = observer((props) => {
|
||||
const { estimateType, estimatePoint, handleEstimatePoint } = props;
|
||||
// state
|
||||
const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false);
|
||||
// ref
|
||||
const EstimatePointValueRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!estimatePointEditToggle)
|
||||
EstimatePointValueRef?.current?.addEventListener("dblclick", () => setEstimatePointEditToggle(true));
|
||||
}, [estimatePointEditToggle]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!estimatePointEditToggle && (
|
||||
<div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-2 text-base">
|
||||
<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">
|
||||
<GripVertical size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
<div ref={EstimatePointValueRef} className="py-2.5 w-full">
|
||||
{estimatePoint?.value ? (
|
||||
estimatePoint?.value
|
||||
) : (
|
||||
<span className="text-custom-text-200">Enter Estimate Value</span>
|
||||
)}
|
||||
</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={() => setEstimatePointEditToggle(true)}
|
||||
>
|
||||
<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={() => handleEstimatePoint("remove", estimatePoint)}
|
||||
>
|
||||
<Trash2 size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{estimatePoint && estimatePointEditToggle && (
|
||||
<EstimatePointItemCreateUpdate
|
||||
estimateType={estimateType}
|
||||
estimatePoint={estimatePoint}
|
||||
updateEstimateValue={(value: string) => handleEstimatePoint("update", { ...estimatePoint, value })}
|
||||
callback={() => setEstimatePointEditToggle(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
90
web/components/estimates/points/create/root.tsx
Normal file
90
web/components/estimates/points/create/root.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { Dispatch, FC, SetStateAction, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||
import { Button, Draggable, Sortable } from "@plane/ui";
|
||||
// components
|
||||
import { EstimatePointItemCreatePreview } from "@/components/estimates/points";
|
||||
// constants
|
||||
import { maxEstimatesCount } from "@/constants/estimates";
|
||||
|
||||
type TEstimatePointCreateRoot = {
|
||||
estimateType: TEstimateSystemKeys;
|
||||
estimatePoints: TEstimatePointsObject[];
|
||||
setEstimatePoints: Dispatch<SetStateAction<TEstimatePointsObject[] | undefined>>;
|
||||
};
|
||||
|
||||
export const EstimatePointCreateRoot: FC<TEstimatePointCreateRoot> = observer((props) => {
|
||||
// props
|
||||
const { estimateType, estimatePoints, setEstimatePoints } = props;
|
||||
|
||||
const handleEstimatePoint = useCallback(
|
||||
(mode: "add" | "remove" | "update", value: TEstimatePointsObject) => {
|
||||
switch (mode) {
|
||||
case "add":
|
||||
setEstimatePoints((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
return [...prevValue, value];
|
||||
});
|
||||
break;
|
||||
case "update":
|
||||
setEstimatePoints((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
return prevValue.map((item) => (item.key === value.key ? { ...item, value: value.value } : item));
|
||||
});
|
||||
break;
|
||||
case "remove":
|
||||
setEstimatePoints((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
return prevValue.filter((item) => item.key !== value.key);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[setEstimatePoints]
|
||||
);
|
||||
|
||||
const handleDragEstimatePoints = (updatedEstimatedOrder: TEstimatePointsObject[]) => {
|
||||
const updatedEstimateKeysOrder = updatedEstimatedOrder.map((item, index) => ({ ...item, key: index + 1 }));
|
||||
setEstimatePoints(updatedEstimateKeysOrder);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-custom-text-200">{estimateType}</div>
|
||||
<Sortable
|
||||
data={estimatePoints}
|
||||
render={(value: TEstimatePointsObject) => (
|
||||
<Draggable data={value}>
|
||||
<EstimatePointItemCreatePreview
|
||||
estimateType={estimateType}
|
||||
estimatePoint={value}
|
||||
handleEstimatePoint={handleEstimatePoint}
|
||||
/>
|
||||
</Draggable>
|
||||
)}
|
||||
onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)}
|
||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||
/>
|
||||
|
||||
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() =>
|
||||
handleEstimatePoint("add", {
|
||||
id: undefined,
|
||||
key: estimatePoints.length + 1,
|
||||
value: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
Add {estimateType}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
107
web/components/estimates/points/create/update.tsx
Normal file
107
web/components/estimates/points/create/update.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, Info, X } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { EEstimateSystem } from "@/constants/estimates";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type TEstimatePointItemCreateUpdate = {
|
||||
estimateType: TEstimateSystemKeys;
|
||||
estimatePoint: TEstimatePointsObject;
|
||||
updateEstimateValue: (value: string) => void;
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
export const EstimatePointItemCreateUpdate: FC<TEstimatePointItemCreateUpdate> = observer((props) => {
|
||||
const { estimateType, estimatePoint, updateEstimateValue, callback } = props;
|
||||
// states
|
||||
const [estimateInputValue, setEstimateInputValue] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || "");
|
||||
}, [estimateInputValue, estimatePoint]);
|
||||
|
||||
const handleClose = () => {
|
||||
setEstimateInputValue("");
|
||||
callback();
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!estimatePoint) return;
|
||||
if (estimateInputValue)
|
||||
try {
|
||||
setError(undefined);
|
||||
|
||||
const currentEstimateType: EEstimateSystem | undefined = estimateType;
|
||||
let isEstimateValid = false;
|
||||
|
||||
if (currentEstimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(currentEstimateType)) {
|
||||
if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) {
|
||||
isEstimateValid = true;
|
||||
}
|
||||
} else if (currentEstimateType && currentEstimateType === EEstimateSystem.CATEGORIES) {
|
||||
if (estimateInputValue && estimateInputValue.length > 0) {
|
||||
isEstimateValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEstimateValid) {
|
||||
updateEstimateValue(estimateInputValue);
|
||||
setError(undefined);
|
||||
handleClose();
|
||||
} else {
|
||||
setError("please enter a valid estimate value");
|
||||
}
|
||||
} catch {
|
||||
setError("something went wrong. please try again later");
|
||||
}
|
||||
else {
|
||||
setError("Please fill the input field");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-2 text-base">
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full border rounded flex items-center",
|
||||
error ? `border-red-500` : `border-custom-border-200`
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={estimateInputValue}
|
||||
onChange={(e) => setEstimateInputValue(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 value"
|
||||
autoFocus
|
||||
/>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</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 text-green-500"
|
||||
onClick={handleCreate}
|
||||
>
|
||||
<Check size={14} />
|
||||
</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={handleClose}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,287 +0,0 @@
|
||||
import { FC, Fragment, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react";
|
||||
import { Select } from "@headlessui/react";
|
||||
import { TEstimatePointsObject } from "@plane/types";
|
||||
import { Draggable, Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { EEstimateUpdateStages } from "@/constants/estimates";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useEstimate, useEstimatePoint } from "@/hooks/store";
|
||||
|
||||
type TEstimatePointItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
estimateId: string | undefined;
|
||||
mode: EEstimateUpdateStages;
|
||||
item: TEstimatePointsObject;
|
||||
editItem: (value: string) => void;
|
||||
replaceEstimateItem: (value: TEstimatePointsObject) => void;
|
||||
deleteItem: () => void;
|
||||
};
|
||||
|
||||
export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId, estimateId, mode, item, editItem, replaceEstimateItem, deleteItem } = props;
|
||||
const { id, key, value } = item;
|
||||
// hooks
|
||||
const { asJson: estimate, creteEstimatePoint, deleteEstimatePoint } = useEstimate(estimateId);
|
||||
const { updateEstimatePoint } = useEstimatePoint(estimateId, id);
|
||||
// ref
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// states
|
||||
const [inputValue, setInputValue] = useState<string | undefined>(undefined);
|
||||
// handling editing states
|
||||
const [estimateEditLoader, setEstimateEditLoader] = useState(false);
|
||||
const [deletedEstimateValue, setDeletedEstimateValue] = useState<string | undefined>(undefined);
|
||||
const [isEstimateEditing, setIsEstimateEditing] = useState(false);
|
||||
const [isEstimateDeleting, setIsEstimateDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (value && inputValue === undefined) setInputValue(value);
|
||||
}, [value, inputValue]);
|
||||
|
||||
const handleCreateEdit = (value: string) => {
|
||||
setInputValue(value);
|
||||
editItem(value);
|
||||
};
|
||||
|
||||
const handleNewEstimatePoint = async () => {
|
||||
if (inputValue) {
|
||||
try {
|
||||
setEstimateEditLoader(true);
|
||||
const estimatePoint = await creteEstimatePoint(workspaceSlug, projectId, { key: key, value: inputValue });
|
||||
if (estimatePoint && estimatePoint.key && estimatePoint.value) {
|
||||
replaceEstimateItem({ id: estimatePoint.id, key: estimatePoint.key, value: estimatePoint.value });
|
||||
}
|
||||
setIsEstimateEditing(false);
|
||||
setEstimateEditLoader(false);
|
||||
} catch (error) {
|
||||
setEstimateEditLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (id) {
|
||||
try {
|
||||
setEstimateEditLoader(true);
|
||||
const estimatePoint = await updateEstimatePoint(workspaceSlug, projectId, {
|
||||
key: key,
|
||||
value: inputValue || "",
|
||||
});
|
||||
if (estimatePoint) if (estimatePoint) editItem(inputValue || "");
|
||||
setIsEstimateEditing(false);
|
||||
setEstimateEditLoader(false);
|
||||
} catch (error) {
|
||||
setEstimateEditLoader(false);
|
||||
}
|
||||
} else {
|
||||
if (inputValue) editItem(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (id) {
|
||||
try {
|
||||
setEstimateEditLoader(true);
|
||||
await deleteEstimatePoint(workspaceSlug, projectId, id, deletedEstimateValue);
|
||||
deleteItem();
|
||||
setIsEstimateDeleting(false);
|
||||
setEstimateEditLoader(false);
|
||||
} catch (error) {
|
||||
setEstimateEditLoader(false);
|
||||
}
|
||||
} else {
|
||||
deleteItem();
|
||||
}
|
||||
};
|
||||
|
||||
const selectDropdownOptions = estimate && estimate?.points ? estimate?.points.filter((point) => point.id !== id) : [];
|
||||
|
||||
return (
|
||||
<Draggable data={item}>
|
||||
{!id && (
|
||||
<>
|
||||
{mode === EEstimateUpdateStages.CREATE && (
|
||||
<div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-2">
|
||||
<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">
|
||||
<GripVertical size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => handleCreateEdit(e.target.value)}
|
||||
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5 w-full"
|
||||
/>
|
||||
<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={handleDelete}
|
||||
>
|
||||
<Trash2 size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === EEstimateUpdateStages.EDIT && (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="w-full border border-custom-border-200 rounded">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className={cn(
|
||||
"border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full",
|
||||
isEstimateDeleting ? `bg-custom-background-90` : `bg-transparent`
|
||||
)}
|
||||
disabled={isEstimateDeleting}
|
||||
/>
|
||||
</div>
|
||||
{estimateEditLoader ? (
|
||||
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</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 text-green-500"
|
||||
onClick={handleNewEstimatePoint}
|
||||
>
|
||||
<Check size={14} />
|
||||
</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={handleDelete}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{id && (
|
||||
<>
|
||||
{mode === EEstimateUpdateStages.EDIT && (
|
||||
<>
|
||||
{!isEstimateEditing && !isEstimateDeleting && (
|
||||
<div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-2">
|
||||
<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">
|
||||
<GripVertical size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
<div className="py-2.5 flex-grow" onClick={() => setIsEstimateEditing(true)}>
|
||||
{value}
|
||||
</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={() => setIsEstimateEditing(true)}
|
||||
>
|
||||
<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={() => setIsEstimateDeleting(true)}
|
||||
>
|
||||
<Trash2 size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isEstimateEditing || isEstimateDeleting) && (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="flex-grow relative flex items-center gap-3">
|
||||
<div className="w-full border border-custom-border-200 rounded">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className={cn(
|
||||
"border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full",
|
||||
isEstimateDeleting ? `bg-custom-background-90` : `bg-transparent`
|
||||
)}
|
||||
disabled={isEstimateDeleting}
|
||||
/>
|
||||
</div>
|
||||
{isEstimateDeleting && (
|
||||
<div className="text-xs relative flex justify-center items-center gap-2 whitespace-nowrap">
|
||||
Mark as <MoveRight size={14} />
|
||||
</div>
|
||||
)}
|
||||
{isEstimateDeleting && (
|
||||
<div className="relative w-full rounded border border-custom-border-200 flex items-center gap-3 p-2.5">
|
||||
<Select
|
||||
className="bg-transparent flex-grow focus:ring-0 focus:border-0 focus:outline-none"
|
||||
value={deletedEstimateValue}
|
||||
onChange={(e) => setDeletedEstimateValue(e.target.value)}
|
||||
>
|
||||
<option value={undefined}>None</option>
|
||||
{selectDropdownOptions.map((option) => (
|
||||
<option key={option?.id} value={option?.value}>
|
||||
{option?.value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{estimateEditLoader ? (
|
||||
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer",
|
||||
isEstimateEditing ? `text-green-500` : `text-red-500`
|
||||
)}
|
||||
onClick={() => (isEstimateEditing ? handleEdit() : handleDelete())}
|
||||
>
|
||||
{isEstimateEditing ? <Check size={14} /> : <Trash2 size={14} />}
|
||||
</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={() => (isEstimateEditing ? setIsEstimateEditing(false) : setIsEstimateDeleting(false))}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === EEstimateUpdateStages.SWITCH && (
|
||||
<div className="flex-grow relative flex items-center gap-3">
|
||||
<div className="flex-grow border border-custom-border-200 rounded">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className="flex-grow border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 bg-custom-background-90 w-full"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<MoveRight size={14} />
|
||||
<div className="flex-grow border border-custom-border-200 rounded">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
export * from "./estimate-point-item";
|
||||
export * from "./inline-editable";
|
||||
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
export * from "./switch";
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { HTMLInputTypeAttribute, useEffect, useRef, useState } from "react";
|
||||
import { Check, X } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
onSave?: (value: string | number) => void;
|
||||
value: string | number;
|
||||
inputType?: HTMLInputTypeAttribute;
|
||||
isEditing?: boolean;
|
||||
};
|
||||
|
||||
export const InlineEdit = ({
|
||||
onSave,
|
||||
value: defaultValue,
|
||||
inputType = "text",
|
||||
isEditing: defaultIsEditing = false,
|
||||
}: Props) => {
|
||||
const [isEditing, setIsEditing] = useState(defaultIsEditing);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Add listener to double click on the div
|
||||
useEffect(() => {
|
||||
wrapperRef?.current?.addEventListener("dblclick", () => {
|
||||
setIsEditing(true);
|
||||
setTimeout(() => {
|
||||
inputRef?.current?.select();
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
if (value) {
|
||||
typeof value === "string" && value.trim();
|
||||
onSave && onSave(value);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
{isEditing ? (
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={inputType}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="flex flex-grow border-custom-border-300 border rounded-sm"
|
||||
/>
|
||||
<Check onClick={handleSave} className="w-6 h-6 bg-custom-primary-100 rounded-sm" />
|
||||
<X
|
||||
onClick={() => {
|
||||
setValue(defaultValue);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
className="w-6 h-6 bg-custom-background-80 rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user