chore: updated ceate estimare workflow

This commit is contained in:
guru_sainath 2024-05-29 11:21:45 +05:30
parent 87d317ab82
commit 39482b72ab
12 changed files with 300 additions and 476 deletions

View File

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

View File

@ -1,3 +1,2 @@
export * from "./modal";
export * from "./stage-one";
export * from "./stage-two";

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export * from "./root";
export * from "./preview";
export * from "./update";

View 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>
);
});

View 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>
);
});

View 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>
);
});

View File

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

View File

@ -1,5 +1,3 @@
export * from "./estimate-point-item";
export * from "./inline-editable";
export * from "./create";
export * from "./edit";
export * from "./switch";

View File

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