chore: integrated new endpoints

This commit is contained in:
guru_sainath 2024-05-27 15:51:27 +05:30
parent be03d90c86
commit 1cd31f4705
16 changed files with 670 additions and 187 deletions

View File

@ -1,4 +1,3 @@
import { IWorkspace, IProject } from "./";
import { EEstimateSystem, EEstimateUpdateStages } from "./enums";
export interface IEstimatePoint {
@ -27,9 +26,8 @@ export interface IEstimate {
type: TEstimateSystemKeys | undefined; // categories, points, time
points: IEstimatePoint[] | undefined;
workspace: string | undefined;
workspace_detail: IWorkspace | undefined;
project: string | undefined;
project_detail: IProject | undefined;
last_used: boolean | undefined;
created_at: Date | undefined;
updated_at: Date | undefined;
created_by: string | undefined;
@ -38,7 +36,9 @@ export interface IEstimate {
export interface IEstimateFormData {
estimate?: {
type: string;
name?: string;
type?: string;
last_used?: boolean;
};
estimate_points: {
id?: string | undefined;

View File

@ -59,7 +59,9 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
if (validatedEstimatePoints.length === estimatePoints?.length) {
const payload: IEstimateFormData = {
estimate: {
name: ESTIMATE_SYSTEMS[estimateSystem]?.name,
type: estimateSystem,
last_used: true,
},
estimate_points: validatedEstimatePoints,
};
@ -122,6 +124,8 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
)}
{estimatePoints && (
<EstimateCreateStageTwo
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateSystem={estimateSystem}
estimatePoints={estimatePoints}
handleEstimatePoints={handleUpdatePoints}

View File

@ -1,4 +1,5 @@
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";
@ -8,13 +9,15 @@ import { EstimatePointItem } from "@/components/estimates";
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> = (props) => {
const { estimateSystem, estimatePoints, handleEstimatePoints } = props;
export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = observer((props) => {
const { workspaceSlug, projectId, estimateSystem, estimatePoints, handleEstimatePoints } = props;
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined;
@ -22,7 +25,7 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
const currentEstimationPoints = estimatePoints;
const newEstimationPoint: TEstimatePointsObject = {
key: currentEstimationPoints.length + 1,
value: "0",
value: "",
};
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
};
@ -59,11 +62,15 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
data={estimatePoints}
render={(value: TEstimatePointsObject, index: number) => (
<EstimatePointItem
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={undefined}
mode={EEstimateUpdateStages.CREATE}
item={value}
estimatePoints={estimatePoints}
editItem={(value: string) => editEstimationPoint(index, value)}
deleteItem={() => deleteEstimationPoint(index)}
handleEstimatePoints={handleEstimatePoints}
/>
)}
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
@ -77,4 +84,4 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
</div>
</div>
);
};
});

View File

@ -17,5 +17,4 @@ export * from "./create";
export * from "./update";
// estimate points
export * from "./points/estimate-point-item";
export * from "./points/inline-editable";
export * from "./points";

View File

@ -0,0 +1,95 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Check, X } from "lucide-react";
import { Spinner } from "@plane/ui";
// constants
import { EEstimateSystem } from "@/constants/estimates";
// hooks
import { useEstimate } from "@/hooks/store";
type TEstimatePointCreate = {
workspaceSlug: string;
projectId: string;
estimateId: string;
estimatePointId: string | undefined;
};
export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) => {
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
// hooks
const { asJson: estimate, estimatePointIds, creteEstimatePoint } = useEstimate(estimateId);
// states
const [loader, setLoader] = useState(false);
const [estimateValue, setEstimateValue] = useState("");
const handleCreate = async () => {
if (estimatePointId) {
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
try {
const estimateType: EEstimateSystem | undefined = estimate?.type;
let isEstimateValid = false;
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) {
isEstimateValid = true;
}
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
if (estimateValue && estimateValue.length > 0) {
isEstimateValid = true;
}
}
if (isEstimateValid) {
setLoader(true);
const payload = {
key: estimatePointIds?.length + 1,
value: estimateValue,
};
await creteEstimatePoint(workspaceSlug, projectId, payload);
setLoader(false);
handleClose();
} else {
console.log("please enter a valid estimate value");
}
} catch {
setLoader(false);
console.log("something went wrong. please try again later");
}
} else {
}
};
const handleClose = () => {
setEstimateValue("");
};
return (
<div className="relative flex items-center gap-2">
<div className="w-full border border-custom-border-200 rounded">
<input
type="text"
value={estimateValue}
onChange={(e) => setEstimateValue(e.target.value)}
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
/>
</div>
{loader ? (
<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={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

@ -0,0 +1,100 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Check, X } from "lucide-react";
import { Spinner } from "@plane/ui";
// constants
import { EEstimateSystem } from "@/constants/estimates";
// hooks
import { useEstimate, useEstimatePoint } from "@/hooks/store";
type TEstimatePointDelete = {
workspaceSlug: string;
projectId: string;
estimateId: string;
estimatePointId: string | undefined;
};
export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) => {
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
// hooks
const { asJson: estimate, estimatePointIds } = useEstimate(estimateId);
const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId);
// states
const [loader, setLoader] = useState(false);
const [estimateValue, setEstimateValue] = useState<string | undefined>(undefined);
useEffect(() => {
if (estimateValue === undefined) setEstimateValue(estimatePoint?.value || "");
}, [estimateValue, estimatePoint]);
const handleCreate = async () => {
if (estimatePointId) {
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
try {
const estimateType: EEstimateSystem | undefined = estimate?.type;
let isEstimateValid = false;
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) {
isEstimateValid = true;
}
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
if (estimateValue && estimateValue.length > 0) {
isEstimateValid = true;
}
}
if (isEstimateValid) {
setLoader(true);
const payload = {
key: estimatePointIds?.length + 1,
value: estimateValue,
};
await updateEstimatePoint(workspaceSlug, projectId, payload);
setLoader(false);
handleClose();
} else {
console.log("please enter a valid estimate value");
}
} catch {
setLoader(false);
console.log("something went wrong. please try again later");
}
} else {
}
};
const handleClose = () => {
setEstimateValue("");
};
return (
<div className="relative flex items-center gap-2">
<div className="w-full border border-custom-border-200 rounded">
<input
type="text"
value={estimateValue}
onChange={(e) => setEstimateValue(e.target.value)}
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
/>
</div>
{loader ? (
<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={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

@ -0,0 +1,29 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Draggable } from "@plane/ui";
// constants
import { EEstimateUpdateStages } from "@/constants/estimates";
type TEstimatePointEditRoot = {
workspaceSlug: string;
projectId: string;
estimateId: string;
mode: EEstimateUpdateStages;
};
type TEstimatePointEditingState = "update" | "delete";
export const EstimatePointEditRoot: FC<TEstimatePointEditRoot> = observer((props) => {
// props
const { workspaceSlug, projectId, estimateId, mode } = props;
// hooks
// states
const [editingState, setEditingState] = useState<TEstimatePointEditingState | undefined>(undefined);
const [estimateEditLoader, setEstimateEditLoader] = useState(false);
const [deletedEstimateValue, setDeletedEstimateValue] = useState<string | undefined>(undefined);
const [isEstimateEditing, setIsEstimateEditing] = useState(false);
const [isEstimateDeleting, setIsEstimateDeleting] = useState(false);
return <Draggable data={item}></Draggable>;
});

View File

@ -8,22 +8,37 @@ import { Draggable, Spinner } from "@plane/ui";
import { EEstimateUpdateStages } from "@/constants/estimates";
// helpers
import { cn } from "@/helpers/common.helper";
import { useEstimate } from "@/hooks/store";
import { useEstimate, useEstimatePoint } from "@/hooks/store";
type TEstimatePointItem = {
workspaceSlug: string;
projectId: string;
estimateId: string | undefined;
mode: EEstimateUpdateStages;
item: TEstimatePointsObject;
estimatePoints: TEstimatePointsObject[];
editItem: (value: string) => void;
deleteItem: () => void;
handleEstimatePoints: (value: TEstimatePointsObject[]) => void;
};
export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
// props
const { estimateId, mode, item, editItem, deleteItem } = props;
const {
workspaceSlug,
projectId,
estimateId,
mode,
item,
estimatePoints,
editItem,
deleteItem,
handleEstimatePoints,
} = props;
const { id, key, value } = item;
// hooks
const { asJson: estimate, updateEstimate, deleteEstimate } = useEstimate(estimateId);
const { asJson: estimate, creteEstimatePoint, deleteEstimatePoint } = useEstimate(estimateId);
const { updateEstimatePoint } = useEstimatePoint(estimateId, id);
// ref
const inputRef = useRef<HTMLInputElement>(null);
// states
@ -35,7 +50,7 @@ export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
const [isEstimateDeleting, setIsEstimateDeleting] = useState(false);
useEffect(() => {
if (inputValue === undefined || inputValue != value) setInputValue(value);
if (value && inputValue === undefined) setInputValue(value);
}, [value, inputValue]);
const handleCreateEdit = (value: string) => {
@ -43,11 +58,26 @@ export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
editItem(value);
};
const handleNewEstimatePoint = async () => {
if (inputValue) {
try {
setEstimateEditLoader(true);
const estimatePoint = await creteEstimatePoint(workspaceSlug, projectId, { key: key, value: inputValue });
if (estimatePoint)
handleEstimatePoints([...estimatePoints, { id: estimatePoint.id, key: key, value: inputValue }]);
setIsEstimateEditing(false);
setEstimateEditLoader(false);
} catch (error) {
setEstimateEditLoader(false);
}
}
};
const handleEdit = async () => {
if (id) {
try {
setEstimateEditLoader(true);
await updateEstimate({ estimate_points: [{ id: id, key: key, value: value }] });
await updateEstimatePoint(workspaceSlug, projectId, { key: key, value: inputValue });
setIsEstimateEditing(false);
setEstimateEditLoader(false);
} catch (error) {
@ -62,7 +92,7 @@ export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
if (id) {
try {
setEstimateEditLoader(true);
await deleteEstimate(deletedEstimateValue);
await deleteEstimatePoint(workspaceSlug, projectId, id, deletedEstimateValue);
setIsEstimateDeleting(false);
setEstimateEditLoader(false);
} catch (error) {
@ -78,24 +108,65 @@ export const EstimatePointItem: FC<TEstimatePointItem> = observer((props) => {
return (
<Draggable data={item}>
{!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" />
</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.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 && (

View File

@ -0,0 +1,8 @@
export * from "./estimate-point-item";
export * from "./inline-editable";
export * from "./edit-root";
export * from "./create";
export * from "./update";
export * from "./delete";

View File

@ -0,0 +1,100 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Check, X } from "lucide-react";
import { Spinner } from "@plane/ui";
// constants
import { EEstimateSystem } from "@/constants/estimates";
// hooks
import { useEstimate, useEstimatePoint } from "@/hooks/store";
type TEstimatePointUpdate = {
workspaceSlug: string;
projectId: string;
estimateId: string;
estimatePointId: string | undefined;
};
export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) => {
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
// hooks
const { asJson: estimate, estimatePointIds } = useEstimate(estimateId);
const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId);
// states
const [loader, setLoader] = useState(false);
const [estimateValue, setEstimateValue] = useState<string | undefined>(undefined);
useEffect(() => {
if (estimateValue === undefined) setEstimateValue(estimatePoint?.value || "");
}, [estimateValue, estimatePoint]);
const handleCreate = async () => {
if (estimatePointId) {
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
try {
const estimateType: EEstimateSystem | undefined = estimate?.type;
let isEstimateValid = false;
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) {
isEstimateValid = true;
}
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
if (estimateValue && estimateValue.length > 0) {
isEstimateValid = true;
}
}
if (isEstimateValid) {
setLoader(true);
const payload = {
key: estimatePointIds?.length + 1,
value: estimateValue,
};
await updateEstimatePoint(workspaceSlug, projectId, payload);
setLoader(false);
handleClose();
} else {
console.log("please enter a valid estimate value");
}
} catch {
setLoader(false);
console.log("something went wrong. please try again later");
}
} else {
}
};
const handleClose = () => {
setEstimateValue("");
};
return (
<div className="relative flex items-center gap-2">
<div className="w-full border border-custom-border-200 rounded">
<input
type="text"
value={estimateValue}
onChange={(e) => setEstimateValue(e.target.value)}
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
/>
</div>
{loader ? (
<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={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,13 +1,12 @@
import { FC, useEffect, useMemo, useState } from "react";
import orderBy from "lodash/orderBy";
import { observer } from "mobx-react";
import { ChevronLeft } from "lucide-react";
import { IEstimateFormData, TEstimatePointsObject, TEstimateUpdateStageKeys, TEstimateSystemKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { TEstimatePointsObject, TEstimateUpdateStageKeys } from "@plane/types";
import { Button } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { EstimateUpdateStageOne, EstimateUpdateStageTwo } from "@/components/estimates";
// constants
import { EEstimateSystem } from "@/constants/estimates";
// hooks
import {
useEstimate,
@ -26,18 +25,19 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
// props
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
// hooks
const { asJson: currentEstimate, updateEstimate } = useEstimate(estimateId);
const { asJson: currentEstimate } = useEstimate(estimateId);
// states
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
if (currentEstimate?.points && currentEstimate?.points.length > 0) {
const estimateValidatePoints: TEstimatePointsObject[] = [];
let estimateValidatePoints: TEstimatePointsObject[] = [];
currentEstimate?.points.map(
(point) =>
point.key && point.value && estimateValidatePoints.push({ id: point.id, key: point.key, value: point.value })
);
estimateValidatePoints = orderBy(estimateValidatePoints, ["key"], ["asc"]);
if (estimateValidatePoints.length > 0) {
setEstimateEditType(type);
setEstimatePoints(estimateValidatePoints);
@ -56,57 +56,6 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
// derived values
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
const isNewEstimatePointsToCreate =
(estimatePoints || []).filter((point) => point.id === undefined).length > 0 ? true : false;
const handleUpdateEstimate = async () => {
try {
if (!workspaceSlug || !projectId || !estimateId || currentEstimate?.type === undefined) return;
const currentEstimatePoints = (estimatePoints || []).filter((point) => point.id === undefined);
const currentEstimationType: TEstimateSystemKeys = currentEstimate?.type;
const validatedEstimatePoints: TEstimatePointsObject[] = [];
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(currentEstimationType)) {
currentEstimatePoints?.map((estimatePoint) => {
if (
estimatePoint.value &&
((estimatePoint.value != "0" && Number(estimatePoint.value)) || estimatePoint.value === "0")
)
validatedEstimatePoints.push(estimatePoint);
});
} else {
currentEstimatePoints?.map((estimatePoint) => {
if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint);
});
}
if (validatedEstimatePoints.length === currentEstimatePoints?.length) {
const payload: IEstimateFormData = {
estimate_points: validatedEstimatePoints,
};
await updateEstimate(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Estimate system created",
message: "Created and Enabled successfully",
});
handleClose();
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "something went wrong",
});
}
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "something went wrong",
});
}
};
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
@ -135,6 +84,8 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
{!estimateEditType && <EstimateUpdateStageOne handleEstimateEditType={handleEstimateEditType} />}
{estimateEditType && estimatePoints && (
<EstimateUpdateStageTwo
workspaceSlug={workspaceSlug}
projectId={projectId}
estimate={currentEstimate}
estimateEditType={estimateEditType}
estimatePoints={estimatePoints}
@ -147,11 +98,6 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
{isNewEstimatePointsToCreate && (
<Button variant="primary" size="sm" onClick={handleUpdateEstimate}>
Update Estimate
</Button>
)}
</div>
</div>
</ModalCore>

View File

@ -1,4 +1,5 @@
import { FC } from "react";
import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react";
import { Plus } from "lucide-react";
import { IEstimate, TEstimatePointsObject, TEstimateUpdateStageKeys } from "@plane/types";
@ -9,6 +10,8 @@ import { EstimatePointItem } from "@/components/estimates";
import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates";
type TEstimateUpdateStageTwo = {
workspaceSlug: string;
projectId: string;
estimate: IEstimate;
estimateEditType: TEstimateUpdateStageKeys | undefined;
estimatePoints: TEstimatePointsObject[];
@ -16,15 +19,15 @@ type TEstimateUpdateStageTwo = {
};
export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = observer((props) => {
const { estimate, estimateEditType, estimatePoints, handleEstimatePoints } = props;
const { workspaceSlug, projectId, estimate, estimateEditType, estimatePoints, handleEstimatePoints } = props;
const currentEstimateSystem = estimate || undefined;
const addNewEstimationPoint = () => {
const currentEstimationPoints = estimatePoints;
const currentEstimationPoints = cloneDeep(estimatePoints);
const newEstimationPoint: TEstimatePointsObject = {
key: currentEstimationPoints.length + 1,
value: "0",
value: "",
};
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
};
@ -64,11 +67,15 @@ export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = observer((pro
data={estimatePoints}
render={(value: TEstimatePointsObject, index: number) => (
<EstimatePointItem
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={estimate?.id || undefined}
mode={estimateEditType}
item={value}
estimatePoints={estimatePoints}
editItem={(value: string) => editEstimationPoint(index, value)}
deleteItem={() => deleteEstimationPoint(index)}
handleEstimatePoints={handleEstimatePoints}
/>
)}
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}

View File

@ -1,5 +1,5 @@
// types
import { IEstimate, IEstimateFormData } from "@plane/types";
import { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
@ -10,7 +10,6 @@ export class EstimateService extends APIService {
super(API_BASE_URL);
}
// fetching the estimates in workspace level
async fetchWorkspaceEstimates(workspaceSlug: string): Promise<IEstimate[] | undefined> {
try {
const { data } = await this.get(`/api/workspaces/${workspaceSlug}/estimates/`);
@ -61,7 +60,7 @@ export class EstimateService extends APIService {
workspaceSlug: string,
projectId: string,
estimateId: string,
payload: IEstimateFormData
payload: Partial<IEstimate>
): Promise<IEstimate | undefined> {
try {
const { data } = await this.patch(
@ -74,16 +73,51 @@ export class EstimateService extends APIService {
}
}
async createEstimatePoint(
workspaceSlug: string,
projectId: string,
estimateId: string,
payload: Partial<IEstimatePoint>
): Promise<IEstimatePoint | undefined> {
try {
const { data } = await this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-points/`,
payload
);
return data || undefined;
} catch (error) {
throw error;
}
}
async updateEstimatePoint(
workspaceSlug: string,
projectId: string,
estimateId: string,
estimatePointId: string,
payload: Partial<IEstimatePoint>
): Promise<IEstimatePoint | undefined> {
try {
const { data } = await this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-points/${estimatePointId}/`,
payload
);
return data || undefined;
} catch (error) {
throw error;
}
}
async removeEstimatePoint(
workspaceSlug: string,
projectId: string,
estimateId: string,
estimatePointId: string,
payload: { new_estimate_id: string | undefined }
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-point/${estimatePointId}/`,
payload
params?: { new_estimate_id: string | undefined }
): Promise<void> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-points/${estimatePointId}/`,
params
)
.then((response) => response?.data)
.catch((error) => {

View File

@ -1,5 +1,6 @@
import { action, computed, makeObservable, observable } from "mobx";
import { IEstimateFormData, IEstimate, IEstimatePoint as IEstimatePointType } from "@plane/types";
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { IEstimate, IEstimatePoint as IEstimatePointType } from "@plane/types";
// services
import { EstimateService } from "@/services/project/estimate.service";
// store
@ -16,7 +17,11 @@ export interface IEstimatePoint extends IEstimatePointType {
// computed
asJson: IEstimatePointType;
// actions
updateEstimatePoint: (payload: IEstimateFormData) => Promise<void>;
updateEstimatePoint: (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimatePointType>
) => Promise<IEstimatePointType | undefined>;
}
export class EstimatePoint implements IEstimatePoint {
@ -73,7 +78,6 @@ export class EstimatePoint implements IEstimatePoint {
this.updated_at = this.data.updated_at;
this.created_by = this.data.created_by;
this.updated_by = this.data.updated_by;
// service
this.service = new EstimateService();
}
@ -96,10 +100,36 @@ export class EstimatePoint implements IEstimatePoint {
}
// actions
updateEstimatePoint = async (payload: IEstimateFormData) => {
/**
* @description updating an estimate point
* @param { Partial<IEstimatePointType> } payload
* @returns { IEstimatePointType | undefined }
*/
updateEstimatePoint = async (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimatePointType>
): Promise<IEstimatePointType | undefined> => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !this.projectEstimate?.id || !this.id || !payload) return undefined;
if (!this.projectEstimate?.id || !this.id || !payload) return undefined;
const estimatePoint = await this.service.updateEstimatePoint(
workspaceSlug,
projectId,
this.projectEstimate?.id,
this.id,
payload
);
if (estimatePoint) {
runInAction(() => {
Object.keys(payload).map((key) => {
const estimatePointKey = key as keyof IEstimatePointType;
set(this, estimatePointKey, estimatePoint[estimatePointKey]);
});
});
}
return estimatePoint;
} catch (error) {
throw error;
}

View File

@ -1,16 +1,8 @@
import set from "lodash/set";
import unset from "lodash/unset";
import update from "lodash/update";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import {
IEstimate as IEstimateType,
IEstimatePoint as IEstimatePointType,
IProject,
IWorkspace,
TEstimateSystemKeys,
IEstimateFormData,
} from "@plane/types";
import { IEstimate as IEstimateType, IEstimatePoint as IEstimatePointType, TEstimateSystemKeys } from "@plane/types";
// services
import { EstimateService } from "@/services/project/estimate.service";
// store
@ -31,8 +23,22 @@ export interface IEstimate extends IEstimateType {
estimatePointIds: string[] | undefined;
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
// actions
updateEstimate: (payload: IEstimateFormData) => Promise<void>;
deleteEstimate: (estimatePointId: string | undefined) => Promise<void>;
updateEstimate: (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimateType>
) => Promise<IEstimateType | undefined>;
creteEstimatePoint: (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimatePointType>
) => Promise<IEstimatePointType | undefined>;
deleteEstimatePoint: (
workspaceSlug: string,
projectId: string,
estimatePointId: string,
newEstimatePointId: string | undefined
) => Promise<void>;
}
export class Estimate implements IEstimate {
@ -43,9 +49,8 @@ export class Estimate implements IEstimate {
type: TEstimateSystemKeys | undefined = undefined;
points: IEstimatePointType[] | undefined = undefined;
workspace: string | undefined = undefined;
workspace_detail: IWorkspace | undefined = undefined;
project: string | undefined = undefined;
project_detail: IProject | undefined = undefined;
last_used: boolean | undefined = undefined;
created_at: Date | undefined = undefined;
updated_at: Date | undefined = undefined;
created_by: string | undefined = undefined;
@ -68,9 +73,8 @@ export class Estimate implements IEstimate {
type: observable.ref,
points: observable,
workspace: observable.ref,
workspace_detail: observable,
project: observable.ref,
project_detail: observable,
last_used: observable.ref,
created_at: observable.ref,
updated_at: observable.ref,
created_by: observable.ref,
@ -83,7 +87,8 @@ export class Estimate implements IEstimate {
estimatePointIds: computed,
// actions
updateEstimate: action,
deleteEstimate: action,
creteEstimatePoint: action,
deleteEstimatePoint: action,
});
this.id = this.data.id;
this.name = this.data.name;
@ -91,14 +96,12 @@ export class Estimate implements IEstimate {
this.type = this.data.type;
this.points = this.data.points;
this.workspace = this.data.workspace;
this.workspace_detail = this.data.workspace_detail;
this.project = this.data.project;
this.project_detail = this.data.project_detail;
this.last_used = this.data.last_used;
this.created_at = this.data.created_at;
this.updated_at = this.data.updated_at;
this.created_by = this.data.created_by;
this.updated_by = this.data.updated_by;
this.data.points?.forEach((estimationPoint) => {
if (estimationPoint.id)
set(this.estimatePoints, [estimationPoint.id], new EstimatePoint(this.store, this.data, estimationPoint));
@ -116,9 +119,8 @@ export class Estimate implements IEstimate {
type: this.type,
points: this.points,
workspace: this.workspace,
workspace_detail: this.workspace_detail,
project: this.project,
project_detail: this.project_detail,
last_used: this.last_used,
created_at: this.created_at,
updated_at: this.updated_at,
created_by: this.created_by,
@ -143,35 +145,95 @@ export class Estimate implements IEstimate {
});
// actions
updateEstimate = async (payload: IEstimateFormData) => {
/**
* @description update an estimate
* @param { string } workspaceSlug
* @param { string } projectId
* @param { Partial<IEstimateType> } payload
* @returns { IEstimateType | undefined }
*/
updateEstimate = async (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimateType>
): Promise<IEstimateType | undefined> => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !this.id || !payload) return;
if (!this.id || !payload) return;
await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload);
const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload);
if (estimate) {
runInAction(() => {
Object.keys(payload).map((key) => {
const estimateKey = key as keyof IEstimateType;
set(this, estimateKey, estimate[estimateKey]);
});
});
}
// runInAction(() => {
// this.points = payload.estimate_points;
// this.data.points = payload.estimate_points;
// });
return estimate;
} catch (error) {
throw error;
}
};
deleteEstimate = async (estimatePointId: string | undefined) => {
/**
* @description create an estimate point
* @param { string } workspaceSlug
* @param { string } projectId
* @param { Partial<IEstimatePointType> } payload
* @returns { IEstimatePointType | undefined }
*/
creteEstimatePoint = async (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimatePointType>
): Promise<IEstimatePointType | undefined> => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !estimatePointId) return;
if (!this.id || !payload) return;
// make delete estimation request
const estimatePoint = await this.service.createEstimatePoint(workspaceSlug, projectId, this.id, payload);
if (estimatePoint) {
runInAction(() => {
if (estimatePoint.id) {
set(this.estimatePoints, [estimatePoint.id], new EstimatePoint(this.store, this.data, estimatePoint));
}
});
}
} catch (error) {
throw error;
}
};
/**
* @description delete an estimate point
* @param { string } workspaceSlug
* @param { string } projectId
* @param { string } estimatePointId
* @param { string | undefined } newEstimatePointId
* @returns { void }
*/
deleteEstimatePoint = async (
workspaceSlug: string,
projectId: string,
estimatePointId: string,
newEstimatePointId: string | undefined
) => {
try {
if (!this.id) return;
const deleteEstimatePoint = await this.service.removeEstimatePoint(
workspaceSlug,
projectId,
this.id,
estimatePointId,
newEstimatePointId ? { new_estimate_id: newEstimatePointId } : undefined
);
runInAction(() => {
update(this, "points", (estimationPoints = []) =>
estimationPoints.filter((point: IEstimatePointType) => point.id !== estimatePointId)
);
unset(this.estimatePoints, [estimatePointId]);
});
return deleteEstimatePoint;
} catch (error) {
throw error;
}

View File

@ -1,5 +1,5 @@
import orderBy from "lodash/orderBy";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
import update from "lodash/update";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
@ -59,7 +59,6 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
// computed
currentActiveEstimateId: computed,
archivedEstimateIds: computed,
projectEstimateIds: computed,
// actions
getWorkspaceEstimates: action,
getProjectEstimates: action,
@ -79,9 +78,10 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
get currentActiveEstimateId(): string | undefined {
const { projectId } = this.store.router;
if (!projectId) return undefined;
const projectDetails = this.store.projectRoot.project.getProjectById(projectId);
if (!projectDetails) return undefined;
return projectDetails.estimate ?? undefined;
const currentActiveEstimateId = Object.values(this.estimates || {}).find(
(p) => p.project === projectId && p.last_used
);
return currentActiveEstimateId?.id ?? undefined;
}
/**
@ -91,25 +91,15 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
get archivedEstimateIds(): string[] | undefined {
const { projectId } = this.store.router;
if (!projectId) return undefined;
const archivedEstimateIds = Object.values(this.estimates || {})
.filter((p) => p.project === projectId && p.id !== this.currentActiveEstimateId)
.map((p) => p.id) as string[];
const archivedEstimates = orderBy(
Object.values(this.estimates || {}).filter((p) => p.project === projectId && !p.last_used),
["created_at"],
"desc"
);
const archivedEstimateIds = archivedEstimates.map((p) => p.id) as string[];
return archivedEstimateIds ?? undefined;
}
/**
* @description get all estimate ids for a project
* @returns { string[] | undefined }
*/
get projectEstimateIds(): string[] | undefined {
const { projectId } = this.store.router;
if (!projectId) return undefined;
const projectEstimatesIds = Object.values(this.estimates || {})
.filter((p) => p.project === projectId)
.map((p) => p.id) as string[];
return projectEstimatesIds ?? undefined;
}
/**
* @description get estimates are enabled in the project or not
* @returns { boolean }
@ -145,6 +135,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
// actions
/**
* @description fetch all estimates for a workspace
* @param { string } workspaceSlug
* @returns { IEstimateType[] | undefined }
*/
getWorkspaceEstimates = async (
@ -153,7 +144,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
): Promise<IEstimateType[] | undefined> => {
try {
this.error = undefined;
if (!this.projectEstimateIds) this.loader = loader ? loader : "init-loader";
if (Object.keys(this.estimates || {}).length <= 0) this.loader = loader ? loader : "init-loader";
const estimates = await this.service.fetchWorkspaceEstimates(workspaceSlug);
if (estimates && estimates.length > 0) {
@ -176,6 +167,8 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
/**
* @description fetch all estimates for a project
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { IEstimateType[] | undefined }
*/
getProjectEstimates = async (
@ -185,7 +178,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
): Promise<IEstimateType[] | undefined> => {
try {
this.error = undefined;
if (!this.projectEstimateIds) this.loader = loader ? loader : "init-loader";
if (!this.estimateIdsByProjectId(projectId)) this.loader = loader ? loader : "init-loader";
const estimates = await this.service.fetchProjectEstimates(workspaceSlug, projectId);
if (estimates && estimates.length > 0) {
@ -208,6 +201,8 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
/**
* @description update an estimate for a project
* @param { string } workspaceSlug
* @param { string } projectId
* @param { string } estimateId
* @returns IEstimateType | undefined
*/
@ -241,7 +236,9 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
/**
* @description create an estimate for a project
* @param { Partial<IEstimateType> } payload
* @param { string } workspaceSlug
* @param { string } projectId
* @param { Partial<IEstimateFormData> } payload
* @returns
*/
createEstimate = async (
@ -253,21 +250,15 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
this.error = undefined;
const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload);
// FIXME: i am getting different response from the server and once backend changes remove the get request and uncomment the commented code
let estimates = await this.getProjectEstimates(workspaceSlug, projectId, "mutation-loader");
estimates = sortBy(estimates, "created_at");
if (estimates && estimates.length > 0)
console.log("estimate", estimate);
if (estimate) {
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
estimate: estimates[estimates.length - 1].id,
estimate: estimate.id,
});
// if (estimate) {
// await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
// estimate: estimate.id,
// });
// runInAction(() => {
// if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
// });
// }
runInAction(() => {
if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
});
}
return estimate;
} catch (error) {