mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
[WEB-522] feat: revamp estimates (#394)
* Move code from EE to CE repo * chore: folder structure updates * Move sortabla and radio input to packages/ui * chore: updated empty and loading screens * chore: delete an estimate point * chore: estimate point response change * chore: updated create estimate and handled the build error * chore: migration fixes * chore: updated create estimate * chore: create estimate workflow update * chore: editing and deleting the existing estimate updates * chore: updating the new estinates in update modal * chore: ui changed * chore: response changes of get and post * chore: new field added in estimates * chore: individual endpoint for estimate points * chore: typo changes * chore: create estimate point * chore: integrated new endpoints * chore: update key value pair * chore: update sorting in the estimates * Add custom option in the estimate templates * chore: handled current project active estimate * chore: handle estimate update worklfow * chore: handled estimates switch * chore: handled estimate edit * chore: handled close button in estimate edit * chore: updated ceate estimare workflow * chore: updated switch estimate * chore: UI and typos * chore: resolved build error * chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point * chore: handled inline errors in the estimate switch * chore: handled active and availability vadilation * chore: handled create and update components in projecr estimates * chore: added migration * Add category specific values for custom template * chore: estimate dropdown handled in issues * chore: estimate alerts * chore: updated alerts * Extract the list row actions * fix: updated and handled the estimate points * fix: upgrader ee banner * Fix issues with sortable * Fix sortable spacing issue in create estimate modal * fix: updated the issue create sorting * chore: removed radio button from ui and updated in the estimates * chore: resolved import error in packaged ui * chore: handled props in create modal * chore: enabled estimated edit and ui changes * chore: handled repeated values in estimate switch * chore: handled estimate create and edit modal * chore: resolved conflicts and added estimate update and switch in ee * chore: resolved conflicts on estimates * chore: resolved file imports * chore: updated file intendation * chore: space fixes * chore: updated error handling in the estimate update * chore: updated error handling in the estimate switch * chore: updated yarn.lock * chore: updated yarn.lock indent --------- Co-authored-by: Satish Gandham <satish.iitg@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
@@ -1 +1,28 @@
|
||||
export * from "ce/components/estimates/estimate-list-item-buttons";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Pen } from "lucide-react";
|
||||
|
||||
type TEstimateListItem = {
|
||||
estimateId: string;
|
||||
isAdmin: boolean;
|
||||
isEstimateEnabled: boolean;
|
||||
isEditable: boolean;
|
||||
onEditClick?: (estimateId: string) => void;
|
||||
onDeleteClick?: (estimateId: string) => void;
|
||||
};
|
||||
|
||||
export const EstimateListItemButtons: FC<TEstimateListItem> = observer((props) => {
|
||||
const { estimateId, isAdmin, isEditable, onEditClick } = props;
|
||||
|
||||
if (!isAdmin || !isEditable) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<button
|
||||
className="relative flex-shrink-0 w-6 h-6 flex justify-center items-center rounded cursor-pointer transition-colors overflow-hidden hover:bg-custom-background-80"
|
||||
onClick={() => onEditClick && onEditClick(estimateId)}
|
||||
>
|
||||
<Pen size={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./estimate-list-item-buttons";
|
||||
export * from "./update";
|
||||
export * from "./points";
|
||||
|
||||
236
web/ee/components/estimates/points/edit-root.tsx
Normal file
236
web/ee/components/estimates/points/edit-root.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Dispatch, FC, Fragment, SetStateAction, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronLeft, Plus } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateTypeError, TEstimateUpdateStageKeys } from "@plane/types";
|
||||
import { Button, Sortable, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EstimatePointItemPreview, EstimatePointCreate } from "@/components/estimates/points";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { EEstimateUpdateStages, estimateCount } from "@/plane-web/constants/estimates";
|
||||
|
||||
type TEstimatePointEditRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
estimateId: string;
|
||||
mode?: EEstimateUpdateStages;
|
||||
setEstimateEditType?: Dispatch<SetStateAction<TEstimateUpdateStageKeys | undefined>>;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const EstimatePointEditRoot: FC<TEstimatePointEditRoot> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId, estimateId, setEstimateEditType, handleClose } = props;
|
||||
// hooks
|
||||
const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSortOrder } = useEstimate(estimateId);
|
||||
// states
|
||||
const [estimatePointCreate, setEstimatePointCreate] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||
const [estimatePointError, setEstimatePointError] = useState<TEstimateTypeError>(undefined);
|
||||
|
||||
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 handleEstimatePointCreate = (mode: "add" | "remove", value: TEstimatePointsObject) => {
|
||||
switch (mode) {
|
||||
case "add":
|
||||
setEstimatePointCreate((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
return [...prevValue, value];
|
||||
});
|
||||
break;
|
||||
case "remove":
|
||||
setEstimatePointCreate((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
return prevValue.filter((item) => item.key !== value.key);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEstimatePoints = async (updatedEstimatedOrder: TEstimatePointsObject[]) => {
|
||||
try {
|
||||
const updatedEstimateKeysOrder = updatedEstimatedOrder.map((item, index) => ({ ...item, key: index + 1 }));
|
||||
await updateEstimateSortOrder(workspaceSlug, projectId, updatedEstimateKeysOrder);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Estimates re-ordered",
|
||||
message: "Estimates have been re-ordered in your project.",
|
||||
});
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Estimate re-order failed",
|
||||
message: "We were unable to re-order the estimates, please try again",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEstimatePointError = (
|
||||
key: number,
|
||||
oldValue: string,
|
||||
newValue: string,
|
||||
message: string | undefined,
|
||||
mode: "add" | "delete" = "add"
|
||||
) => {
|
||||
setEstimatePointError((prev) => {
|
||||
if (mode === "add") {
|
||||
return { ...prev, [key]: { oldValue, newValue, message } };
|
||||
} else {
|
||||
const newError = { ...prev };
|
||||
delete newError[key];
|
||||
return newError;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
if (estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= estimateCount.max - 1) {
|
||||
const currentKey = estimatePoints.length + (estimatePointCreate?.length || 0) + 1;
|
||||
handleEstimatePointCreate("add", {
|
||||
id: undefined,
|
||||
key: currentKey,
|
||||
value: "",
|
||||
});
|
||||
handleEstimatePointError && handleEstimatePointError(currentKey, "", "", undefined, "add");
|
||||
}
|
||||
};
|
||||
|
||||
const validateEstimatePointError = () => {
|
||||
let estimateError = false;
|
||||
if (!estimatePointError) return estimateError;
|
||||
|
||||
Object.keys(estimatePointError || {}).forEach((key) => {
|
||||
const currentKey = key as unknown as number;
|
||||
if (
|
||||
estimatePointError[currentKey]?.oldValue != estimatePointError[currentKey]?.newValue ||
|
||||
estimatePointError[currentKey]?.newValue === "" ||
|
||||
estimatePointError[currentKey]?.message
|
||||
) {
|
||||
estimateError = true;
|
||||
}
|
||||
});
|
||||
|
||||
return estimateError;
|
||||
};
|
||||
|
||||
const handleEstimateDone = async () => {
|
||||
if (!validateEstimatePointError()) {
|
||||
handleClose();
|
||||
} else {
|
||||
setEstimatePointError((prev) => {
|
||||
const newError = { ...prev };
|
||||
Object.keys(newError || {}).forEach((key) => {
|
||||
const currentKey = key as unknown as number;
|
||||
if (
|
||||
newError[currentKey]?.newValue != "" &&
|
||||
newError[currentKey]?.oldValue === newError[currentKey]?.newValue
|
||||
) {
|
||||
delete newError[currentKey];
|
||||
} else {
|
||||
// NOTE: validate the estimate type
|
||||
newError[currentKey].message =
|
||||
newError[currentKey].message ||
|
||||
"Estimate point can't be empty. Enter a value in each field or remove those you don't have values for.";
|
||||
}
|
||||
});
|
||||
return newError;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !estimateId) return <></>;
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-between items-center gap-2 px-5">
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div
|
||||
onClick={() => setEstimateEditType && setEstimateEditType(undefined)}
|
||||
className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="text-xl font-medium text-custom-text-200">Edit estimate system</div>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" size="sm" onClick={handleEstimateDone}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 px-5">
|
||||
<div className="text-sm font-medium text-custom-text-200 capitalize">{estimate?.type}</div>
|
||||
<div>
|
||||
<Sortable
|
||||
data={estimatePoints}
|
||||
render={(value: TEstimatePointsObject) => (
|
||||
<Fragment>
|
||||
{value?.id && estimate?.type ? (
|
||||
<EstimatePointItemPreview
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateId={estimateId}
|
||||
estimatePointId={value?.id}
|
||||
estimateType={estimate?.type}
|
||||
estimatePoint={value}
|
||||
estimatePoints={estimatePoints}
|
||||
estimatePointError={estimatePointError?.[value.key] || undefined}
|
||||
handleEstimatePointError={(
|
||||
newValue: string,
|
||||
message: string | undefined,
|
||||
mode: "add" | "delete" = "add"
|
||||
) =>
|
||||
handleEstimatePointError &&
|
||||
handleEstimatePointError(value.key, value.value, newValue, message, mode)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Fragment>
|
||||
)}
|
||||
onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)}
|
||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{estimatePointCreate &&
|
||||
estimatePointCreate.map(
|
||||
(estimatePoint) =>
|
||||
estimate?.type && (
|
||||
<EstimatePointCreate
|
||||
key={estimatePoint?.key}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateId={estimateId}
|
||||
estimateType={estimate?.type}
|
||||
closeCallBack={() => handleEstimatePointCreate("remove", estimatePoint)}
|
||||
estimatePoints={estimatePoints}
|
||||
handleCreateCallback={() => estimatePointCreate.length === 1 && handleCreate()}
|
||||
estimatePointError={estimatePointError?.[estimatePoint.key] || undefined}
|
||||
handleEstimatePointError={(
|
||||
newValue: string,
|
||||
message: string | undefined,
|
||||
mode: "add" | "delete" = "add"
|
||||
) =>
|
||||
handleEstimatePointError &&
|
||||
handleEstimatePointError(estimatePoint.key, estimatePoint.value, newValue, message, mode)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= estimateCount.max - 1 && (
|
||||
<Button variant="link-primary" size="sm" prependIcon={<Plus />} onClick={handleCreate}>
|
||||
Add {estimate?.type}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
2
web/ee/components/estimates/points/index.ts
Normal file
2
web/ee/components/estimates/points/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./edit-root";
|
||||
export * from "./switch";
|
||||
2
web/ee/components/estimates/points/switch/index.ts
Normal file
2
web/ee/components/estimates/points/switch/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./preview";
|
||||
65
web/ee/components/estimates/points/switch/preview.tsx
Normal file
65
web/ee/components/estimates/points/switch/preview.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Info, MoveRight } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateTypeErrorObject } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useEstimatePoint } from "@/hooks/store";
|
||||
|
||||
type TEstimatePointItemSwitchPreview = {
|
||||
estimateId: string;
|
||||
estimatePointId: string | undefined;
|
||||
estimatePoint: TEstimatePointsObject;
|
||||
handleEstimatePoint: (value: string) => void;
|
||||
estimatePointError?: TEstimateTypeErrorObject | undefined;
|
||||
};
|
||||
|
||||
export const EstimatePointItemSwitchPreview: FC<TEstimatePointItemSwitchPreview> = observer((props) => {
|
||||
const {
|
||||
estimateId,
|
||||
estimatePointId,
|
||||
estimatePoint: currentEstimatePoint,
|
||||
handleEstimatePoint,
|
||||
estimatePointError,
|
||||
} = props;
|
||||
// hooks
|
||||
const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId);
|
||||
|
||||
if (!estimatePoint) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="w-full border border-custom-border-200 rounded p-2.5 bg-custom-background-90">
|
||||
{estimatePoint?.value}
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center">
|
||||
<MoveRight size={12} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full border rounded flex items-center",
|
||||
estimatePointError?.message ? `border-red-500` : `border-custom-border-200`
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={currentEstimatePoint?.value}
|
||||
onChange={(e) => handleEstimatePoint(e.target.value)}
|
||||
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||
autoFocus
|
||||
placeholder="Enter estimate point value"
|
||||
/>
|
||||
{estimatePointError?.message && (
|
||||
<>
|
||||
<Tooltip tooltipContent={estimatePointError?.message} 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>
|
||||
);
|
||||
});
|
||||
224
web/ee/components/estimates/points/switch/root.tsx
Normal file
224
web/ee/components/estimates/points/switch/root.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import {
|
||||
IEstimateFormData,
|
||||
TEstimatePointsObject,
|
||||
TEstimateSystemKeys,
|
||||
TEstimateTypeError,
|
||||
TEstimateUpdateStageKeys,
|
||||
} from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { EstimatePointItemSwitchPreview } from "@/plane-web/components/estimates";
|
||||
// plane web constants
|
||||
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS } from "@/plane-web/constants/estimates";
|
||||
|
||||
type TEstimatePointSwitchRoot = {
|
||||
setEstimateEditType?: Dispatch<SetStateAction<TEstimateUpdateStageKeys | undefined>>;
|
||||
estimateSystemSwitchType: TEstimateSystemKeys;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
estimateId: string;
|
||||
handleClose: () => void;
|
||||
mode?: EEstimateUpdateStages;
|
||||
};
|
||||
|
||||
export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((props) => {
|
||||
// props
|
||||
const { setEstimateEditType, estimateSystemSwitchType, workspaceSlug, projectId, estimateId, handleClose } = props;
|
||||
// hooks
|
||||
const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSwitch } = useEstimate(estimateId);
|
||||
// states
|
||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||
const [estimatePointError, setEstimatePointError] = useState<TEstimateTypeError>(undefined);
|
||||
|
||||
const handleEstimatePointError = (
|
||||
key: number,
|
||||
oldValue: string,
|
||||
newValue: string,
|
||||
message: string | undefined,
|
||||
mode: "add" | "delete" = "add"
|
||||
) => {
|
||||
setEstimatePointError((prev) => {
|
||||
if (mode === "add") {
|
||||
return { ...prev, [key]: { oldValue, newValue, message } };
|
||||
} else {
|
||||
const newError = { ...prev };
|
||||
delete newError[key];
|
||||
return newError;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!estimatePointIds) return;
|
||||
setEstimatePoints(
|
||||
estimatePointIds.map((estimatePointId: string) => {
|
||||
const estimatePoint = estimatePointById(estimatePointId);
|
||||
if (estimatePoint) return { id: estimatePointId, key: estimatePoint.key, value: "" };
|
||||
}) as TEstimatePointsObject[]
|
||||
);
|
||||
}, [estimatePointById, estimatePointIds]);
|
||||
|
||||
const handleEstimatePoints = (index: number, value: string) => {
|
||||
setEstimatePoints((prevValue) => {
|
||||
prevValue = prevValue ? [...prevValue] : [];
|
||||
prevValue[index].value = value;
|
||||
handleEstimatePointError(prevValue[index].key, "", "", undefined, "delete");
|
||||
return prevValue;
|
||||
});
|
||||
};
|
||||
|
||||
const isValidEstimatePoints = (estimateSystemSwitchType: TEstimateSystemKeys) => {
|
||||
let isValid: boolean = false;
|
||||
|
||||
// validate if the fields are empty
|
||||
const isNonEmptyPoints = [];
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimatePoint.value && estimatePoint.value != "" && estimatePoint.value.length > 0) {
|
||||
isNonEmptyPoints.push(estimatePoint.value);
|
||||
} else {
|
||||
handleEstimatePointError(estimatePoint.key, "", "", "Please fill this estimate point field");
|
||||
}
|
||||
});
|
||||
if (isNonEmptyPoints.length === estimatePoints?.length) {
|
||||
isValid = true;
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// validate if fields are repeated
|
||||
const repeatedValues: string[] = [];
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimatePoint.value && estimatePoint.value != "") {
|
||||
if (repeatedValues.includes(estimatePoint.value)) {
|
||||
handleEstimatePointError(estimatePoint.key, "", "", "Estimate point value cannot be repeated");
|
||||
} else {
|
||||
repeatedValues.push(estimatePoint.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (repeatedValues.length === estimatePoints?.length) {
|
||||
isValid = true;
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// validate if fields are valid in points and time required number values and categories required string values
|
||||
const estimatePointArray: string[] = [];
|
||||
if ([(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(estimateSystemSwitchType)) {
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimateSystemSwitchType && estimatePoint.value && estimatePoint.value != "") {
|
||||
if (Number(estimatePoint.value) && Number(estimatePoint.value) >= 0) {
|
||||
estimatePointArray.push(estimatePoint.value);
|
||||
} else {
|
||||
handleEstimatePointError(estimatePoint.key, "", "", "Estimate point value should be a number.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (estimateSystemSwitchType === EEstimateSystem.CATEGORIES) {
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimatePoint.value && estimatePoint.value != "") {
|
||||
if (estimatePoint.value.length > 0 && isNaN(Number(estimatePoint.value))) {
|
||||
estimatePointArray.push(estimatePoint.value);
|
||||
} else {
|
||||
handleEstimatePointError(estimatePoint.key, "", "", "Estimate point value should be a string.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (estimatePointArray.length === estimatePoints?.length) {
|
||||
isValid = true;
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSwitchEstimate = async () => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const isEstimatesValid = estimateSystemSwitchType && isValidEstimatePoints(estimateSystemSwitchType);
|
||||
|
||||
if (isEstimatesValid) {
|
||||
const validatedEstimatePoints: TEstimatePointsObject[] = [];
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
validatedEstimatePoints.push(estimatePoint);
|
||||
});
|
||||
|
||||
const payload: IEstimateFormData = {
|
||||
estimate: {
|
||||
name: ESTIMATE_SYSTEMS[estimateSystemSwitchType]?.name,
|
||||
type: estimateSystemSwitchType,
|
||||
},
|
||||
estimate_points: validatedEstimatePoints,
|
||||
};
|
||||
await updateEstimateSwitch(workspaceSlug, projectId, payload);
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Estimate system created",
|
||||
message: "Created and Enabled successfully",
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "something went wrong",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !estimateId || !estimatePoints) return <></>;
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-between items-center gap-2 px-5">
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div
|
||||
onClick={() => setEstimateEditType && setEstimateEditType(undefined)}
|
||||
className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="text-xl font-medium text-custom-text-200">Edit estimate system</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 px-5 pb-5">
|
||||
<div className="text-sm font-medium flex items-center gap-2">
|
||||
<div className="w-full">Current {estimate?.type}</div>
|
||||
<div className="flex-shrink-0 w-4 h-4" />
|
||||
<div className="w-full">New {estimateSystemSwitchType}</div>
|
||||
</div>
|
||||
|
||||
{estimatePoints.map((estimateObject, index) => (
|
||||
<EstimatePointItemSwitchPreview
|
||||
key={estimateObject?.id}
|
||||
estimateId={estimateId}
|
||||
estimatePointId={estimateObject?.id}
|
||||
estimatePoint={estimateObject}
|
||||
handleEstimatePoint={(value: string) => handleEstimatePoints(index, value)}
|
||||
estimatePointError={estimatePointError?.[estimateObject.key] || undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative flex justify-end items-center gap-3 px-5 pt-5 border-t border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" size="sm" onClick={handleSwitchEstimate}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./modal";
|
||||
export * from "./stage-one";
|
||||
|
||||
@@ -1 +1,86 @@
|
||||
export * from "ce/components/estimates/update/modal";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TEstimateSystemKeys, TEstimateUpdateStageKeys } from "@plane/types";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
// plane web components
|
||||
import {
|
||||
EstimateUpdateStageOne,
|
||||
EstimatePointEditRoot,
|
||||
EstimatePointSwitchRoot,
|
||||
} from "@/plane-web/components/estimates";
|
||||
// plane web constants
|
||||
import { EEstimateSystem, EEstimateUpdateStages } from "@/plane-web/constants/estimates";
|
||||
|
||||
type TUpdateEstimateModal = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
estimateId: string | undefined;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
|
||||
// hooks
|
||||
const { asJson: estimate } = useEstimate(estimateId);
|
||||
// states
|
||||
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
|
||||
const [estimateSystemSwitchType, setEstimateSystemSwitchType] = useState<TEstimateSystemKeys | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setEstimateEditType(undefined);
|
||||
setEstimateSystemSwitchType(undefined);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
|
||||
if (type === EEstimateUpdateStages.SWITCH && estimate?.type)
|
||||
setEstimateSystemSwitchType(
|
||||
estimate?.type === EEstimateSystem.CATEGORIES ? EEstimateSystem.POINTS : EEstimateSystem.CATEGORIES
|
||||
);
|
||||
setEstimateEditType(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<div className="relative space-y-6 py-5">
|
||||
{!estimateEditType && (
|
||||
<EstimateUpdateStageOne
|
||||
estimateEditType={estimateEditType}
|
||||
handleEstimateEditType={handleEstimateEditType}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{estimateEditType && estimateId && (
|
||||
<>
|
||||
{estimateEditType === EEstimateUpdateStages.EDIT && (
|
||||
<EstimatePointEditRoot
|
||||
setEstimateEditType={setEstimateEditType}
|
||||
handleClose={handleClose}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateId={estimateId}
|
||||
/>
|
||||
)}
|
||||
{estimateEditType === EEstimateUpdateStages.SWITCH && estimateSystemSwitchType && (
|
||||
<EstimatePointSwitchRoot
|
||||
setEstimateEditType={setEstimateEditType}
|
||||
estimateSystemSwitchType={estimateSystemSwitchType}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
estimateId={estimateId}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
||||
52
web/ee/components/estimates/update/stage-one.tsx
Normal file
52
web/ee/components/estimates/update/stage-one.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { TEstimateUpdateStageKeys } from "@plane/types";
|
||||
import { Button } from "@plane/ui";
|
||||
// plane web constants
|
||||
import { ESTIMATE_OPTIONS_STAGE_ONE } from "@/plane-web/constants/estimates";
|
||||
|
||||
type TEstimateUpdateStageOne = {
|
||||
estimateEditType?: TEstimateUpdateStageKeys;
|
||||
handleClose: () => void;
|
||||
handleEstimateEditType?: (stage: TEstimateUpdateStageKeys) => void;
|
||||
};
|
||||
|
||||
export const EstimateUpdateStageOne: FC<TEstimateUpdateStageOne> = (props) => {
|
||||
const { estimateEditType, handleClose, handleEstimateEditType } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-between items-center gap-2 px-5">
|
||||
<div className="text-xl font-medium text-custom-text-200">Edit estimate system</div>
|
||||
</div>
|
||||
|
||||
{!estimateEditType && (
|
||||
<div className="space-y-3 px-5">
|
||||
{ESTIMATE_OPTIONS_STAGE_ONE &&
|
||||
Object.keys(ESTIMATE_OPTIONS_STAGE_ONE).map((stage) => {
|
||||
const currentStage = stage as TEstimateUpdateStageKeys;
|
||||
return (
|
||||
<div
|
||||
key={currentStage}
|
||||
className="border border-custom-border-300 cursor-pointer space-y-1 p-3 rounded transition-colors"
|
||||
onClick={() => handleEstimateEditType && handleEstimateEditType(currentStage)}
|
||||
>
|
||||
<h3 className="text-base font-medium">{ESTIMATE_OPTIONS_STAGE_ONE?.[currentStage]?.title}</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{ESTIMATE_OPTIONS_STAGE_ONE?.[currentStage]?.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative flex justify-end items-center gap-3 px-5 pt-5 border-t border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,39 @@
|
||||
export * from "ce/constants/estimates";
|
||||
import { EEstimateSystem, EEstimateUpdateStages, estimateCount, ESTIMATE_SYSTEMS } from "ce/constants/estimates";
|
||||
|
||||
estimateCount.max = 12;
|
||||
|
||||
ESTIMATE_SYSTEMS.points.templates.fibonacci.values = [
|
||||
...ESTIMATE_SYSTEMS.points.templates.fibonacci.values,
|
||||
{ id: undefined, key: 7, value: "21" },
|
||||
{ id: undefined, key: 8, value: "34" },
|
||||
{ id: undefined, key: 9, value: "55" },
|
||||
];
|
||||
|
||||
ESTIMATE_SYSTEMS.points.templates.linear.values = [
|
||||
...ESTIMATE_SYSTEMS.points.templates.linear.values,
|
||||
{ id: undefined, key: 7, value: "7" },
|
||||
{ id: undefined, key: 8, value: "8" },
|
||||
{ id: undefined, key: 9, value: "9" },
|
||||
];
|
||||
|
||||
ESTIMATE_SYSTEMS.points.templates.squares.values = [
|
||||
...ESTIMATE_SYSTEMS.points.templates.squares.values,
|
||||
{ id: undefined, key: 7, value: "49" },
|
||||
{ id: undefined, key: 8, value: "64" },
|
||||
{ id: undefined, key: 9, value: "81" },
|
||||
];
|
||||
|
||||
export { EEstimateSystem, EEstimateUpdateStages, estimateCount, ESTIMATE_SYSTEMS };
|
||||
|
||||
export const ESTIMATE_OPTIONS_STAGE_ONE: Partial<
|
||||
Record<EEstimateUpdateStages, { title: string; description: string }>
|
||||
> = {
|
||||
[EEstimateUpdateStages.EDIT]: {
|
||||
title: "Add, update or remove estimates",
|
||||
description: "Manage current system either adding, updating or removing the points or categories.",
|
||||
},
|
||||
[EEstimateUpdateStages.SWITCH]: {
|
||||
title: "Change estimate type",
|
||||
description: "Convert your points system to categories system and vice versa.",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user