chore: editing and deleting the existing estimate updates

This commit is contained in:
guru_sainath 2024-05-27 11:35:49 +05:30
parent 1e59d5b735
commit bdb0674d35
6 changed files with 213 additions and 173 deletions

View File

@ -5,7 +5,7 @@ import { Button, Sortable } from "@plane/ui";
// components
import { EstimatePointItem } from "@/components/estimates";
// constants
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS } from "@/constants/estimates";
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS, maxEstimatesCount } from "@/constants/estimates";
type TEstimateCreateStageTwo = {
estimateSystem: EEstimateSystem;
@ -17,7 +17,6 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
const { estimateSystem, estimatePoints, handleEstimatePoints } = props;
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined;
const maxEstimatesCount = 11;
const addNewEstimationPoint = () => {
const currentEstimationPoints = estimatePoints;
@ -60,6 +59,7 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
data={estimatePoints}
render={(value: TEstimatePointsObject, index: number) => (
<EstimatePointItem
estimateId={undefined}
mode={EEstimateUpdateStages.CREATE}
item={value}
editItem={(value: string) => editEstimationPoint(index, value)}

View File

@ -1,67 +1,83 @@
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 } from "@plane/ui";
import { Draggable, Spinner } from "@plane/ui";
// constants
import { EEstimateUpdateStages } from "@/constants/estimates";
// components
import { InlineEdit } from "./inline-editable";
// helpers
import { cn } from "@/helpers/common.helper";
import { useEstimate } from "@/hooks/store";
type TEstimatePointItem = {
estimateId: string | undefined;
mode: EEstimateUpdateStages;
item: TEstimatePointsObject;
editItem: (value: string) => void;
deleteItem: () => void;
};
const EstimatePointItem: FC<TEstimatePointItem> = (props) => {
export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
// props
const { mode, item, editItem, deleteItem } = props;
const { estimateId, mode, item, editItem, deleteItem } = props;
const { id, key, value } = item;
// hooks
const { asJson: estimate, updateEstimate, deleteEstimate } = useEstimate(estimateId);
// ref
const inputRef = useRef<HTMLInputElement>(null);
// states
const [inputValue, setInputValue] = useState<string | undefined>(undefined);
const [isEditing, setIsEditing] = useState(false);
const [showDeleteUI, setShowDeleteUI] = useState(false);
// 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 (inputValue === undefined) setInputValue(value);
if (inputValue === undefined || inputValue != value) setInputValue(value);
}, [value, inputValue]);
const handleSave = () => {
if (id) {
// Make the api call to save the estimate point
// Show a spinner
setIsEditing(false);
}
};
const handleEdit = (value: string) => {
if (id) {
setIsEditing(true);
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
});
} else {
const handleCreateEdit = (value: string) => {
setInputValue(value);
editItem(value);
};
const handleEdit = async () => {
if (id) {
try {
setEstimateEditLoader(true);
await updateEstimate({ estimate_points: [{ id: id, key: key, value: value }] });
setIsEstimateEditing(false);
setEstimateEditLoader(false);
} catch (error) {
setEstimateEditLoader(false);
}
} else {
if (inputValue) editItem(inputValue);
}
};
const handleDelete = () => {
const handleDelete = async () => {
if (id) {
setShowDeleteUI(true);
try {
setEstimateEditLoader(true);
await deleteEstimate(deletedEstimateValue);
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}>
{mode === EEstimateUpdateStages.CREATE && (
{!id && (
<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" />
@ -70,8 +86,8 @@ const EstimatePointItem: FC<TEstimatePointItem> = (props) => {
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => handleEdit(e.target.value)}
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
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"
@ -82,122 +98,120 @@ const EstimatePointItem: FC<TEstimatePointItem> = (props) => {
</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>
<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 py-2.5"
/>
<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={handleDelete}
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={handleDelete}
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="flex-grow border border-custom-border-200 rounded">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className={cn(
"flex-grow 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 && <MoveRight size={14} />}
{isEstimateDeleting && (
<div className="relative flex-grow 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="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="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 bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
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
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}
>
<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={handleDelete}
>
<Trash2 size={14} className="text-custom-text-200" />
</div>
</div>
)}
{/* <div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-3">
<GripVertical size={14} className="text-custom-text-200" />
<MoveRight size={14} />
<div className="flex-grow border border-custom-border-200 rounded">
<input
ref={inputRef}
type="text"
value={value}
onChange={() => {}}
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
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"
/>
<Pencil size={14} className="text-custom-text-200" />
<Trash2 size={14} className="text-custom-text-200" />
<Check size={14} className="text-custom-text-200" />
<X size={14} className="text-custom-text-200" />
</div> */}
{/* {isEditing && (
<div className="flex justify-between items-center gap-4">
<input
type="text"
value={value}
onChange={() => {}}
className="border rounded-md border-custom-border-300 p-3 flex-grow"
ref={inputRef}
/>
<div>
<div className="flex gap-4 justify-between items-center">
<Check className="w-6 h-6" onClick={handleSave} />
<X className="w-6 h-6" onClick={() => setIsEditing(false)} />
</div>
</div>
</div>
)}
{!isEditing && (
<div className="border rounded-md border-custom-border-300 mb-2 p-3 flex justify-between items-center">
<div className="flex items-center">
<GripVertical className="w-4 h-4" />
{!showDeleteUI ? <InlineEdit value={value} /> : value}
{showDeleteUI && (
<Fragment>
<MoveRight className="w-4 h-4 mx-2" />
<Select>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="delayed">Delayed</option>
<option value="canceled">Canceled</option>
</Select>
<Check className="w-4 h-4 rounded-md" />
<X className="w-4 h-4 rounded-md" onClick={() => setShowDeleteUI(false)} />
</Fragment>
</>
)}
</div>
<div className="flex gap-4 items-center">
<Pencil className="w-4 h-4" onClick={handleEdit} />
{!showDeleteUI && <Trash2 className="w-4 h-4" onClick={handleDelete} />}
</div>
</div>
)} */}
</Draggable>
);
};
export { EstimatePointItem };
});

View File

@ -1,5 +1,4 @@
import { FC, useEffect, useMemo, useState } from "react";
import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react";
import { ChevronLeft } from "lucide-react";
import { IEstimateFormData, TEstimatePointsObject, TEstimateUpdateStageKeys, TEstimateSystemKeys } from "@plane/types";
@ -46,10 +45,7 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
}
};
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => {
const points = cloneDeep(newPoints);
setEstimatePoints(points);
};
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
useEffect(() => {
if (!isOpen) {
@ -109,8 +105,6 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
}
};
console.log("estimateStage", estimateEditType);
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">

View File

@ -4,6 +4,8 @@ import { IEstimate, TEstimatePointsObject, TEstimateUpdateStageKeys } from "@pla
import { Button, Sortable } from "@plane/ui";
// components
import { EstimatePointItem } from "@/components/estimates";
// constants
import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates";
type TEstimateUpdateStageTwo = {
estimate: IEstimate;
@ -19,7 +21,6 @@ export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = (props) => {
const addNewEstimationPoint = () => {
const currentEstimationPoints = estimatePoints;
const newEstimationPoint: TEstimatePointsObject = {
key: currentEstimationPoints.length + 1,
value: "0",
@ -27,32 +28,61 @@ export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = (props) => {
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
};
const deleteEstimationPoint = (index: number) => {
const editEstimationPoint = (index: number, value: string) => {
const newEstimationPoints = estimatePoints;
newEstimationPoints.splice(index, 1);
newEstimationPoints[index].value = value;
handleEstimatePoints(newEstimationPoints);
};
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) =>
updatedEstimatePoints.map((item, index) => ({
const deleteEstimationPoint = (index: number) => {
let newEstimationPoints = estimatePoints;
newEstimationPoints.splice(index, 1);
newEstimationPoints = newEstimationPoints.map((item, index) => ({
...item,
key: index + 1,
}));
handleEstimatePoints(newEstimationPoints);
};
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) => {
const sortedEstimatePoints = updatedEstimatePoints.map((item, index) => ({
...item,
key: index + 1,
})) as TEstimatePointsObject[];
return sortedEstimatePoints;
};
if (!estimateEditType) return <></>;
return (
<div className="space-y-4">
<div className="space-y-1">
<div className="text-sm font-medium text-custom-text-300">
{estimateEditType === EEstimateUpdateStages.SWITCH ? "Estimate type switching" : currentEstimateSystem?.type}
</div>
<div className="space-y-3">
<Sortable
data={estimatePoints}
render={(value: TEstimatePointsObject, index: number) => (
<EstimatePointItem item={value} deleteItem={() => deleteEstimationPoint(index)} />
<EstimatePointItem
estimateId={estimate?.id || undefined}
mode={estimateEditType}
item={value}
editItem={(value: string) => editEstimationPoint(index, value)}
deleteItem={() => deleteEstimationPoint(index)}
/>
)}
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
/>
<Button prependIcon={<Plus />} onClick={addNewEstimationPoint}>
Add {currentEstimateSystem?.name}
{estimateEditType === EEstimateUpdateStages.EDIT && (
<>
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
<Button size="sm" prependIcon={<Plus />} onClick={addNewEstimationPoint}>
Add {currentEstimateSystem?.type}
</Button>
)}
</>
)}
</div>
</div>
);
};

View File

@ -1,3 +1,6 @@
// types
import { TEstimateSystems } from "@plane/types";
export enum EEstimateSystem {
POINTS = "points",
CATEGORIES = "categories",
@ -10,8 +13,7 @@ export enum EEstimateUpdateStages {
SWITCH = "switch",
}
// types
import { TEstimateSystems } from "@plane/types";
export const maxEstimatesCount = 11;
export const ESTIMATE_SYSTEMS: TEstimateSystems = {
points: {

View File

@ -28,11 +28,11 @@ export interface IEstimate extends IEstimateType {
estimatePoints: Record<string, IEstimatePoint>;
// computed
asJson: IEstimateType;
EstimatePointIds: string[] | undefined;
estimatePointIds: string[] | undefined;
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
// actions
updateEstimate: (payload: IEstimateFormData) => Promise<void>;
deleteEstimate: (estimatePointId: string) => Promise<void>;
deleteEstimate: (estimatePointId: string | undefined) => Promise<void>;
}
export class Estimate implements IEstimate {
@ -80,7 +80,7 @@ export class Estimate implements IEstimate {
estimatePoints: observable,
// computed
asJson: computed,
EstimatePointIds: computed,
estimatePointIds: computed,
// actions
updateEstimate: action,
deleteEstimate: action,
@ -126,7 +126,7 @@ export class Estimate implements IEstimate {
};
}
get EstimatePointIds() {
get estimatePointIds() {
const { estimatePoints } = this;
if (!estimatePoints) return undefined;
@ -159,7 +159,7 @@ export class Estimate implements IEstimate {
}
};
deleteEstimate = async (estimatePointId: string) => {
deleteEstimate = async (estimatePointId: string | undefined) => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !estimatePointId) return;