mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: handle estimate update worklfow
This commit is contained in:
parent
4fa3eda5cd
commit
f362afb001
@ -64,9 +64,10 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
estimate = request.data.get('estimate')
|
estimate = request.data.get('estimate')
|
||||||
estimate_name = estimate.get("name", generate_random_name())
|
estimate_name = estimate.get("name", generate_random_name())
|
||||||
|
estimate_type = estimate.get("type", 'categories')
|
||||||
last_used = estimate.get("last_used", False)
|
last_used = estimate.get("last_used", False)
|
||||||
estimate = Estimate.objects.create(
|
estimate = Estimate.objects.create(
|
||||||
name=estimate_name, project_id=project_id, last_used=last_used
|
name=estimate_name, project_id=project_id, last_used=last_used, type=estimate_type
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_points = request.data.get("estimate_points", [])
|
estimate_points = request.data.get("estimate_points", [])
|
||||||
|
@ -31,7 +31,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setEstimateSystem(EEstimateSystem.POINTS);
|
setEstimateSystem(EEstimateSystem.CATEGORIES);
|
||||||
setEstimatePoints(undefined);
|
setEstimatePoints(undefined);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@ -42,6 +42,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
const handleCreateEstimate = async () => {
|
const handleCreateEstimate = async () => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const validatedEstimatePoints: TEstimatePointsObject[] = [];
|
const validatedEstimatePoints: TEstimatePointsObject[] = [];
|
||||||
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) {
|
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) {
|
||||||
estimatePoints?.map((estimatePoint) => {
|
estimatePoints?.map((estimatePoint) => {
|
||||||
@ -56,6 +57,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint);
|
if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validatedEstimatePoints.length === estimatePoints?.length) {
|
if (validatedEstimatePoints.length === estimatePoints?.length) {
|
||||||
const payload: IEstimateFormData = {
|
const payload: IEstimateFormData = {
|
||||||
estimate: {
|
estimate: {
|
||||||
@ -80,7 +82,6 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
@ -18,7 +18,7 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
|||||||
|
|
||||||
if (!currentEstimateSystem) return <></>;
|
if (!currentEstimateSystem) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0 gap-2 mb-2">
|
<div className="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0 gap-2 mb-2">
|
||||||
<RadioInput
|
<RadioInput
|
||||||
options={Object.keys(ESTIMATE_SYSTEMS).map((system) => {
|
options={Object.keys(ESTIMATE_SYSTEMS).map((system) => {
|
||||||
@ -36,7 +36,8 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
|||||||
className="mb-4"
|
className="mb-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
|
||||||
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium text-custom-text-200">Start from scratch</div>
|
<div className="text-sm font-medium text-custom-text-200">Start from scratch</div>
|
||||||
<button
|
<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-2 text-left flex-1 w-full block"
|
||||||
@ -47,7 +48,9 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
|||||||
Add your own <span className="lowercase">{currentEstimateSystem.name}</span> from scratch
|
Add your own <span className="lowercase">{currentEstimateSystem.name}</span> from scratch
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium text-custom-text-200">Choose a template</div>
|
<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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||||
{Object.keys(currentEstimateSystem.templates).map((name) =>
|
{Object.keys(currentEstimateSystem.templates).map((name) =>
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,29 +0,0 @@
|
|||||||
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>;
|
|
||||||
});
|
|
@ -1,9 +1,11 @@
|
|||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, Info, X } from "lucide-react";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner, Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EEstimateSystem } from "@/constants/estimates";
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEstimate } from "@/hooks/store";
|
import { useEstimate } from "@/hooks/store";
|
||||||
|
|
||||||
@ -11,66 +13,89 @@ type TEstimatePointCreate = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
estimateId: string;
|
estimateId: string;
|
||||||
estimatePointId: string | undefined;
|
callback: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) => {
|
export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
|
const { workspaceSlug, projectId, estimateId, callback } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { asJson: estimate, estimatePointIds, creteEstimatePoint } = useEstimate(estimateId);
|
const { asJson: estimate, estimatePointIds, creteEstimatePoint } = useEstimate(estimateId);
|
||||||
// states
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
const [estimateValue, setEstimateValue] = useState("");
|
const [estimateInputValue, setEstimateInputValue] = useState("");
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setEstimateInputValue("");
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (estimatePointId) {
|
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
|
||||||
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
|
if (estimateInputValue)
|
||||||
try {
|
try {
|
||||||
|
setLoader(true);
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
const estimateType: EEstimateSystem | undefined = estimate?.type;
|
const estimateType: EEstimateSystem | undefined = estimate?.type;
|
||||||
let isEstimateValid = false;
|
let isEstimateValid = false;
|
||||||
|
|
||||||
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
|
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
|
||||||
if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) {
|
if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) {
|
||||||
isEstimateValid = true;
|
isEstimateValid = true;
|
||||||
}
|
}
|
||||||
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
|
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
|
||||||
if (estimateValue && estimateValue.length > 0) {
|
if (estimateInputValue && estimateInputValue.length > 0) {
|
||||||
isEstimateValid = true;
|
isEstimateValid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEstimateValid) {
|
if (isEstimateValid) {
|
||||||
setLoader(true);
|
|
||||||
const payload = {
|
const payload = {
|
||||||
key: estimatePointIds?.length + 1,
|
key: estimatePointIds?.length + 1,
|
||||||
value: estimateValue,
|
value: estimateInputValue,
|
||||||
};
|
};
|
||||||
await creteEstimatePoint(workspaceSlug, projectId, payload);
|
await creteEstimatePoint(workspaceSlug, projectId, payload);
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
|
setError(undefined);
|
||||||
handleClose();
|
handleClose();
|
||||||
} else {
|
} else {
|
||||||
console.log("please enter a valid estimate value");
|
setLoader(false);
|
||||||
|
setError("please enter a valid estimate value");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
console.log("something went wrong. please try again later");
|
setError("something went wrong. please try again later");
|
||||||
}
|
}
|
||||||
} else {
|
else {
|
||||||
|
setError("Please fill the input field");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setEstimateValue("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center gap-2">
|
<div className="relative flex items-center gap-2 text-base">
|
||||||
<div className="w-full border border-custom-border-200 rounded">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full border rounded flex items-center",
|
||||||
|
error ? `border-red-500` : `border-custom-border-200`
|
||||||
|
)}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={estimateValue}
|
value={estimateInputValue}
|
||||||
onChange={(e) => setEstimateValue(e.target.value)}
|
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"
|
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||||
|
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>
|
||||||
{loader ? (
|
{loader ? (
|
||||||
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
111
web/components/estimates/points/edit/delete.tsx
Normal file
111
web/components/estimates/points/edit/delete.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Info, MoveRight, Trash2, X } from "lucide-react";
|
||||||
|
import { Select } from "@headlessui/react";
|
||||||
|
import { Spinner, Tooltip } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { useEstimate, useEstimatePoint } from "@/hooks/store";
|
||||||
|
|
||||||
|
type TEstimatePointDelete = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
estimateId: string;
|
||||||
|
estimatePointId: string;
|
||||||
|
callback: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props;
|
||||||
|
// hooks
|
||||||
|
const { asJson: estimate, deleteEstimatePoint } = useEstimate(estimateId);
|
||||||
|
const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId);
|
||||||
|
// states
|
||||||
|
const [loader, setLoader] = useState(false);
|
||||||
|
const [estimateInputValue, setEstimateInputValue] = useState<string | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setEstimateInputValue("");
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!workspaceSlug || !projectId || !projectId) return;
|
||||||
|
try {
|
||||||
|
setLoader(true);
|
||||||
|
setError(undefined);
|
||||||
|
await deleteEstimatePoint(workspaceSlug, projectId, estimateId, estimateInputValue);
|
||||||
|
setLoader(false);
|
||||||
|
setError(undefined);
|
||||||
|
handleClose();
|
||||||
|
} catch {
|
||||||
|
setLoader(false);
|
||||||
|
setError("something went wrong. please try again later");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// derived values
|
||||||
|
const selectDropdownOptions =
|
||||||
|
estimate && estimate?.points ? estimate?.points.filter((point) => point.id !== estimatePointId) : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center gap-2 text-base">
|
||||||
|
<div className="flex-grow relative flex items-center gap-3">
|
||||||
|
<div className="w-full border border-custom-border-200 rounded p-2.5 bg-custom-background-90">
|
||||||
|
{estimatePoint?.value}
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center items-center gap-2 whitespace-nowrap">
|
||||||
|
Mark as <MoveRight size={14} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full rounded border flex items-center gap-3 p-2.5",
|
||||||
|
error ? `border-red-500` : `border-custom-border-200`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="bg-transparent flex-grow focus:ring-0 focus:border-0 focus:outline-none"
|
||||||
|
value={estimateInputValue}
|
||||||
|
onChange={(e) => setEstimateInputValue(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value={undefined}>None</option>
|
||||||
|
{selectDropdownOptions.map((option) => (
|
||||||
|
<option key={option?.id} value={option?.value}>
|
||||||
|
{option?.value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{error && (
|
||||||
|
<>
|
||||||
|
<Tooltip tooltipContent={error} position="bottom">
|
||||||
|
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden relative flex justify-center items-center text-red-500">
|
||||||
|
<Info size={14} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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-red-500"
|
||||||
|
onClick={handleCreate}
|
||||||
|
>
|
||||||
|
<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={handleClose}
|
||||||
|
>
|
||||||
|
<X size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
5
web/components/estimates/points/edit/index.ts
Normal file
5
web/components/estimates/points/edit/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./root";
|
||||||
|
export * from "./preview";
|
||||||
|
export * from "./create";
|
||||||
|
export * from "./update";
|
||||||
|
export * from "./delete";
|
78
web/components/estimates/points/edit/preview.tsx
Normal file
78
web/components/estimates/points/edit/preview.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { GripVertical, Pencil, Trash2 } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { EstimatePointUpdate, EstimatePointDelete } from "@/components/estimates/points";
|
||||||
|
// hooks
|
||||||
|
import { useEstimatePoint } from "@/hooks/store";
|
||||||
|
|
||||||
|
type TEstimatePointItemPreview = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
estimateId: string;
|
||||||
|
estimatePointId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimatePointItemPreview: FC<TEstimatePointItemPreview> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
|
||||||
|
// hooks
|
||||||
|
const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId);
|
||||||
|
// state
|
||||||
|
const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false);
|
||||||
|
const [estimatePointDeleteToggle, setEstimatePointDeleteToggle] = useState(false);
|
||||||
|
// ref
|
||||||
|
const EstimatePointValueRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!estimatePointEditToggle && !estimatePointDeleteToggle)
|
||||||
|
EstimatePointValueRef?.current?.addEventListener("dblclick", () => setEstimatePointEditToggle(true));
|
||||||
|
}, [estimatePointDeleteToggle, estimatePointEditToggle]);
|
||||||
|
|
||||||
|
if (!estimatePoint?.id) return <></>;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!estimatePointEditToggle && !estimatePointDeleteToggle && (
|
||||||
|
<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}
|
||||||
|
</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={() => setEstimatePointDeleteToggle(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{estimatePoint && estimatePointEditToggle && (
|
||||||
|
<EstimatePointUpdate
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
estimateId={estimateId}
|
||||||
|
estimatePointId={estimatePointId}
|
||||||
|
callback={() => setEstimatePointEditToggle(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{estimatePoint && estimatePointDeleteToggle && (
|
||||||
|
<EstimatePointDelete
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
estimateId={estimateId}
|
||||||
|
estimatePointId={estimatePointId}
|
||||||
|
callback={() => setEstimatePointDeleteToggle(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
84
web/components/estimates/points/edit/root.tsx
Normal file
84
web/components/estimates/points/edit/root.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { TEstimatePointsObject } from "@plane/types";
|
||||||
|
import { Button, Draggable, Sortable } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { EstimatePointItemPreview, EstimatePointCreate } from "@/components/estimates/points";
|
||||||
|
// constants
|
||||||
|
import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates";
|
||||||
|
// hooks
|
||||||
|
import { useEstimate } from "@/hooks/store";
|
||||||
|
|
||||||
|
type TEstimatePointEditRoot = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
estimateId: string;
|
||||||
|
mode?: EEstimateUpdateStages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimatePointEditRoot: FC<TEstimatePointEditRoot> = observer((props) => {
|
||||||
|
// props
|
||||||
|
const { workspaceSlug, projectId, estimateId } = props;
|
||||||
|
// hooks
|
||||||
|
const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSortOrder } = useEstimate(estimateId);
|
||||||
|
// states
|
||||||
|
const [estimatePointCreateToggle, setEstimatePointCreateToggle] = useState(false);
|
||||||
|
|
||||||
|
const estimatePoints: TEstimatePointsObject[] =
|
||||||
|
estimatePointIds && estimatePointIds.length > 0
|
||||||
|
? (estimatePointIds.map((estimatePointId: string) => {
|
||||||
|
const estimatePoint = estimatePointById(estimatePointId);
|
||||||
|
if (estimatePoint) return { id: estimatePointId, key: estimatePoint.key, value: estimatePoint.value };
|
||||||
|
}) as TEstimatePointsObject[])
|
||||||
|
: ([] as TEstimatePointsObject[]);
|
||||||
|
|
||||||
|
const handleDragEstimatePoints = (updatedEstimatedOrder: TEstimatePointsObject[]) => {
|
||||||
|
const updatedEstimateKeysOrder = updatedEstimatedOrder.map((item, index) => ({ ...item, key: index + 1 }));
|
||||||
|
updateEstimateSortOrder(workspaceSlug, projectId, updatedEstimateKeysOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !estimateId) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm font-medium text-custom-text-200">{estimate?.type}</div>
|
||||||
|
<Sortable
|
||||||
|
data={estimatePoints}
|
||||||
|
render={(value: TEstimatePointsObject) => (
|
||||||
|
<Draggable data={value}>
|
||||||
|
{value?.id && (
|
||||||
|
<EstimatePointItemPreview
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
estimateId={estimateId}
|
||||||
|
estimatePointId={value?.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
)}
|
||||||
|
onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)}
|
||||||
|
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{estimatePointCreateToggle && (
|
||||||
|
<EstimatePointCreate
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
estimateId={estimateId}
|
||||||
|
callback={() => setEstimatePointCreateToggle(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
|
||||||
|
<Button
|
||||||
|
variant="link-primary"
|
||||||
|
size="sm"
|
||||||
|
prependIcon={<Plus />}
|
||||||
|
onClick={() => setEstimatePointCreateToggle(true)}
|
||||||
|
disabled={estimatePointCreateToggle}
|
||||||
|
>
|
||||||
|
Add {estimate?.type}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,9 +1,11 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, Info, X } from "lucide-react";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner, Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EEstimateSystem } from "@/constants/estimates";
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEstimate, useEstimatePoint } from "@/hooks/store";
|
import { useEstimate, useEstimatePoint } from "@/hooks/store";
|
||||||
|
|
||||||
@ -11,71 +13,94 @@ type TEstimatePointUpdate = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
estimateId: string;
|
estimateId: string;
|
||||||
estimatePointId: string | undefined;
|
estimatePointId: string;
|
||||||
|
callback: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) => {
|
export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, estimateId, estimatePointId } = props;
|
const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { asJson: estimate, estimatePointIds } = useEstimate(estimateId);
|
const { asJson: estimate, estimatePointIds } = useEstimate(estimateId);
|
||||||
const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId);
|
const { asJson: estimatePoint, updateEstimatePoint } = useEstimatePoint(estimateId, estimatePointId);
|
||||||
// states
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
const [estimateValue, setEstimateValue] = useState<string | undefined>(undefined);
|
const [estimateInputValue, setEstimateInputValue] = useState<string | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (estimateValue === undefined) setEstimateValue(estimatePoint?.value || "");
|
if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || "");
|
||||||
}, [estimateValue, estimatePoint]);
|
}, [estimateInputValue, estimatePoint]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setEstimateInputValue("");
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (estimatePointId) {
|
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
|
||||||
if (!workspaceSlug || !projectId || !projectId || !estimatePointIds) return;
|
if (estimateInputValue)
|
||||||
try {
|
try {
|
||||||
|
setLoader(true);
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
const estimateType: EEstimateSystem | undefined = estimate?.type;
|
const estimateType: EEstimateSystem | undefined = estimate?.type;
|
||||||
let isEstimateValid = false;
|
let isEstimateValid = false;
|
||||||
|
|
||||||
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
|
if (estimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateType)) {
|
||||||
if (estimateValue && Number(estimateValue) && Number(estimateValue) >= 0) {
|
if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) {
|
||||||
isEstimateValid = true;
|
isEstimateValid = true;
|
||||||
}
|
}
|
||||||
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
|
} else if (estimateType && estimateType === EEstimateSystem.CATEGORIES) {
|
||||||
if (estimateValue && estimateValue.length > 0) {
|
if (estimateInputValue && estimateInputValue.length > 0) {
|
||||||
isEstimateValid = true;
|
isEstimateValid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEstimateValid) {
|
if (isEstimateValid) {
|
||||||
setLoader(true);
|
|
||||||
const payload = {
|
const payload = {
|
||||||
key: estimatePointIds?.length + 1,
|
value: estimateInputValue,
|
||||||
value: estimateValue,
|
|
||||||
};
|
};
|
||||||
await updateEstimatePoint(workspaceSlug, projectId, payload);
|
await updateEstimatePoint(workspaceSlug, projectId, payload);
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
|
setError(undefined);
|
||||||
handleClose();
|
handleClose();
|
||||||
} else {
|
} else {
|
||||||
console.log("please enter a valid estimate value");
|
setLoader(false);
|
||||||
|
setError("please enter a valid estimate value");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
console.log("something went wrong. please try again later");
|
setError("something went wrong. please try again later");
|
||||||
}
|
}
|
||||||
} else {
|
else {
|
||||||
|
setError("Please fill the input field");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setEstimateValue("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center gap-2">
|
<div className="relative flex items-center gap-2 text-base">
|
||||||
<div className="w-full border border-custom-border-200 rounded">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full border rounded flex items-center",
|
||||||
|
error ? `border-red-500` : `border-custom-border-200`
|
||||||
|
)}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={estimateValue}
|
value={estimateInputValue}
|
||||||
onChange={(e) => setEstimateValue(e.target.value)}
|
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"
|
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||||
|
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>
|
||||||
{loader ? (
|
{loader ? (
|
||||||
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
<div className="w-6 h-6 flex-shrink-0 relative flex justify-center items-center rota">
|
@ -1,8 +1,4 @@
|
|||||||
export * from "./estimate-point-item";
|
export * from "./estimate-point-item";
|
||||||
export * from "./inline-editable";
|
export * from "./inline-editable";
|
||||||
|
|
||||||
export * from "./edit-root";
|
export * from "./edit";
|
||||||
|
|
||||||
export * from "./create";
|
|
||||||
export * from "./update";
|
|
||||||
export * from "./delete";
|
|
||||||
|
@ -44,6 +44,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
|||||||
Estimates
|
Estimates
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* current active estimate section */}
|
||||||
{currentActiveEstimateId ? (
|
{currentActiveEstimateId ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* estimates activated deactivated section */}
|
{/* estimates activated deactivated section */}
|
||||||
@ -88,7 +89,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* modals for create and update */}
|
{/* CRUD modals */}
|
||||||
<CreateEstimateModal
|
<CreateEstimateModal
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./stage-one";
|
export * from "./stage-one";
|
||||||
export * from "./stage-two";
|
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import { FC, useEffect, useMemo, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import orderBy from "lodash/orderBy";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
import { TEstimatePointsObject, TEstimateUpdateStageKeys } from "@plane/types";
|
import { TEstimateUpdateStageKeys } from "@plane/types";
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||||
import { EstimateUpdateStageOne, EstimateUpdateStageTwo } from "@/components/estimates";
|
import { EstimatePointEditRoot, EstimateUpdateStageOne } from "@/components/estimates";
|
||||||
// hooks
|
|
||||||
import {
|
|
||||||
useEstimate,
|
|
||||||
// useProjectEstimates
|
|
||||||
} from "@/hooks/store";
|
|
||||||
|
|
||||||
type TUpdateEstimateModal = {
|
type TUpdateEstimateModal = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -24,39 +18,15 @@ type TUpdateEstimateModal = {
|
|||||||
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
|
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
|
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
|
||||||
// hooks
|
|
||||||
const { asJson: currentEstimate } = useEstimate(estimateId);
|
|
||||||
// states
|
// states
|
||||||
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
|
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
|
||||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
|
||||||
|
|
||||||
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
|
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => setEstimateEditType(type);
|
||||||
if (currentEstimate?.points && currentEstimate?.points.length > 0) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) setEstimateEditType(undefined);
|
||||||
setEstimateEditType(undefined);
|
|
||||||
setEstimatePoints(undefined);
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// derived values
|
|
||||||
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||||
<div className="relative space-y-6 py-5">
|
<div className="relative space-y-6 py-5">
|
||||||
@ -65,10 +35,7 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
|
|||||||
<div className="relative flex items-center gap-1">
|
<div className="relative flex items-center gap-1">
|
||||||
{estimateEditType && (
|
{estimateEditType && (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => setEstimateEditType(undefined)}
|
||||||
setEstimateEditType(undefined);
|
|
||||||
handleUpdatePoints(undefined);
|
|
||||||
}}
|
|
||||||
className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center"
|
className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
@ -76,29 +43,17 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
|
|||||||
)}
|
)}
|
||||||
<div className="text-xl font-medium text-custom-text-200">Edit estimate system</div>
|
<div className="text-xl font-medium text-custom-text-200">Edit estimate system</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">Step {renderEstimateStepsCount}/2</div>
|
<Button variant="primary" size="sm" onClick={handleClose}>
|
||||||
</div>
|
Done
|
||||||
|
|
||||||
{/* estimate steps */}
|
|
||||||
<div className="px-5">
|
|
||||||
{!estimateEditType && <EstimateUpdateStageOne handleEstimateEditType={handleEstimateEditType} />}
|
|
||||||
{estimateEditType && estimatePoints && (
|
|
||||||
<EstimateUpdateStageTwo
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
estimate={currentEstimate}
|
|
||||||
estimateEditType={estimateEditType}
|
|
||||||
estimatePoints={estimatePoints}
|
|
||||||
handleEstimatePoints={handleUpdatePoints}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex justify-end items-center gap-3 px-5 pt-5 border-t border-custom-border-100">
|
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 pb-1">
|
||||||
|
{!estimateEditType && <EstimateUpdateStageOne handleEstimateEditType={handleEstimateEditType} />}
|
||||||
|
{estimateEditType && estimateId && (
|
||||||
|
<EstimatePointEditRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalCore>
|
</ModalCore>
|
||||||
);
|
);
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
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";
|
|
||||||
import { Button, Sortable } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { EstimatePointItem } from "@/components/estimates";
|
|
||||||
// constants
|
|
||||||
import { EEstimateUpdateStages, maxEstimatesCount } from "@/constants/estimates";
|
|
||||||
import { useEstimate } from "@/hooks/store";
|
|
||||||
|
|
||||||
type TEstimateUpdateStageTwo = {
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
estimate: IEstimate;
|
|
||||||
estimateEditType: TEstimateUpdateStageKeys | undefined;
|
|
||||||
estimatePoints: TEstimatePointsObject[];
|
|
||||||
handleEstimatePoints: (value: TEstimatePointsObject[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = observer((props) => {
|
|
||||||
const { workspaceSlug, projectId, estimate, estimateEditType, estimatePoints, handleEstimatePoints } = props;
|
|
||||||
// hooks
|
|
||||||
const { updateEstimate: updateEstimateRequest } = useEstimate(estimate?.id);
|
|
||||||
|
|
||||||
const currentEstimateSystem = estimate || undefined;
|
|
||||||
|
|
||||||
const addNewEstimationPoint = () => {
|
|
||||||
const currentEstimationPoints = cloneDeep(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 = cloneDeep(estimatePoints);
|
|
||||||
newEstimationPoints.splice(index, 1);
|
|
||||||
newEstimationPoints = newEstimationPoints.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
key: index + 1,
|
|
||||||
}));
|
|
||||||
handleEstimatePoints(newEstimationPoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceEstimateItem = (index: number, value: TEstimatePointsObject) => {
|
|
||||||
const newEstimationPoints = cloneDeep(estimatePoints);
|
|
||||||
newEstimationPoints[index].id = value.id;
|
|
||||||
newEstimationPoints[index].key = value.key;
|
|
||||||
newEstimationPoints[index].value = value.value;
|
|
||||||
handleEstimatePoints(newEstimationPoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) => {
|
|
||||||
try {
|
|
||||||
const sortedEstimatePoints = cloneDeep(updatedEstimatePoints).map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
key: index + 1,
|
|
||||||
})) as TEstimatePointsObject[];
|
|
||||||
handleEstimatePoints(sortedEstimatePoints);
|
|
||||||
updateEstimateRequest(workspaceSlug, projectId, { estimate_points: sortedEstimatePoints });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!estimateEditType) return <></>;
|
|
||||||
return (
|
|
||||||
<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
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
estimateId={estimate?.id || undefined}
|
|
||||||
mode={estimateEditType}
|
|
||||||
item={value}
|
|
||||||
editItem={(value: string) => editEstimationPoint(index, value)}
|
|
||||||
replaceEstimateItem={(value: TEstimatePointsObject) => replaceEstimateItem(index, value)}
|
|
||||||
deleteItem={() => deleteEstimationPoint(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onChange={(data: TEstimatePointsObject[]) => updatedSortedKeys(data)}
|
|
||||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
|
||||||
/>
|
|
||||||
{estimateEditType === EEstimateUpdateStages.EDIT && (
|
|
||||||
<>
|
|
||||||
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
|
|
||||||
<Button size="sm" prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
|
||||||
Add {currentEstimateSystem?.type}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -61,7 +61,7 @@ export class EstimateService extends APIService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
estimateId: string,
|
estimateId: string,
|
||||||
payload: Partial<IEstimateFormData>
|
payload: Partial<IEstimateFormData>
|
||||||
): Promise<IEstimate | undefined> {
|
): Promise<{ points: IEstimatePoint[] } | undefined> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.patch(
|
const { data } = await this.patch(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import orderBy from "lodash/orderBy";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import unset from "lodash/unset";
|
import unset from "lodash/unset";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
IEstimatePoint as IEstimatePointType,
|
IEstimatePoint as IEstimatePointType,
|
||||||
TEstimateSystemKeys,
|
TEstimateSystemKeys,
|
||||||
IEstimateFormData,
|
IEstimateFormData,
|
||||||
|
TEstimatePointsObject,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// services
|
// services
|
||||||
import { EstimateService } from "@/services/project/estimate.service";
|
import { EstimateService } from "@/services/project/estimate.service";
|
||||||
@ -26,13 +28,18 @@ export interface IEstimate extends IEstimateType {
|
|||||||
// computed
|
// computed
|
||||||
asJson: IEstimateType;
|
asJson: IEstimateType;
|
||||||
estimatePointIds: string[] | undefined;
|
estimatePointIds: string[] | undefined;
|
||||||
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
|
estimatePointById: (estimatePointId: string) => IEstimatePointType | undefined;
|
||||||
// actions
|
// actions
|
||||||
updateEstimate: (
|
updateEstimate: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
payload: Partial<IEstimateFormData>
|
payload: Partial<IEstimateFormData>
|
||||||
) => Promise<IEstimateType | undefined>;
|
) => Promise<IEstimateType | undefined>;
|
||||||
|
updateEstimateSortOrder: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
payload: TEstimatePointsObject[]
|
||||||
|
) => Promise<void>;
|
||||||
creteEstimatePoint: (
|
creteEstimatePoint: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -92,6 +99,7 @@ export class Estimate implements IEstimate {
|
|||||||
estimatePointIds: computed,
|
estimatePointIds: computed,
|
||||||
// actions
|
// actions
|
||||||
updateEstimate: action,
|
updateEstimate: action,
|
||||||
|
updateEstimateSortOrder: action,
|
||||||
creteEstimatePoint: action,
|
creteEstimatePoint: action,
|
||||||
deleteEstimatePoint: action,
|
deleteEstimatePoint: action,
|
||||||
});
|
});
|
||||||
@ -136,11 +144,11 @@ export class Estimate implements IEstimate {
|
|||||||
get estimatePointIds() {
|
get estimatePointIds() {
|
||||||
const { estimatePoints } = this;
|
const { estimatePoints } = this;
|
||||||
if (!estimatePoints) return undefined;
|
if (!estimatePoints) return undefined;
|
||||||
|
let currentEstimatePoints = Object.values(estimatePoints).filter(
|
||||||
const estimatePointIds = Object.values(estimatePoints)
|
(estimatePoint) => estimatePoint?.estimate === this.id
|
||||||
.map((point) => point.estimate && this.id)
|
);
|
||||||
.filter((id) => id) as string[];
|
currentEstimatePoints = orderBy(currentEstimatePoints, ["key"], "asc");
|
||||||
|
const estimatePointIds = currentEstimatePoints.map((estimatePoint) => estimatePoint.id) as string[];
|
||||||
return estimatePointIds ?? undefined;
|
return estimatePointIds ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,16 +174,38 @@ export class Estimate implements IEstimate {
|
|||||||
if (!this.id || !payload) return;
|
if (!this.id || !payload) return;
|
||||||
|
|
||||||
const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload);
|
const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload);
|
||||||
if (estimate) {
|
return estimate as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description update an estimate sort order
|
||||||
|
* @param { string } workspaceSlug
|
||||||
|
* @param { string } projectId
|
||||||
|
* @param { Partial<IEstimateFormData> } payload
|
||||||
|
* @returns { void }
|
||||||
|
*/
|
||||||
|
updateEstimateSortOrder = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
payload: TEstimatePointsObject[]
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!this.id || !payload) return;
|
||||||
|
|
||||||
|
const estimatePoints = await this.service.updateEstimate(workspaceSlug, projectId, this.id, {
|
||||||
|
estimate_points: payload,
|
||||||
|
});
|
||||||
|
if (estimatePoints?.points && estimatePoints?.points.length > 0) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
Object.keys(payload).map((key) => {
|
estimatePoints?.points.map((estimatePoint) => {
|
||||||
const estimateKey = key as keyof IEstimateType;
|
if (estimatePoint.id)
|
||||||
set(this, estimateKey, estimate[estimateKey]);
|
set(this.estimatePoints, [estimatePoint.id], new EstimatePoint(this.store, this.data, estimatePoint));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return estimate;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -162,6 +162,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
status: "error",
|
status: "error",
|
||||||
message: "Error fetching estimates",
|
message: "Error fetching estimates",
|
||||||
};
|
};
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,6 +197,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
status: "error",
|
status: "error",
|
||||||
message: "Error fetching estimates",
|
message: "Error fetching estimates",
|
||||||
};
|
};
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -231,6 +233,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
status: "error",
|
status: "error",
|
||||||
message: "Error fetching estimate by id",
|
message: "Error fetching estimate by id",
|
||||||
};
|
};
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -250,7 +253,6 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
|
|
||||||
const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload);
|
const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload);
|
||||||
console.log("estimate", estimate);
|
|
||||||
if (estimate) {
|
if (estimate) {
|
||||||
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
|
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
|
||||||
estimate: estimate.id,
|
estimate: estimate.id,
|
||||||
@ -266,6 +268,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
status: "error",
|
status: "error",
|
||||||
message: "Error creating estimate",
|
message: "Error creating estimate",
|
||||||
};
|
};
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user