mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: create estimate workflow update
This commit is contained in:
parent
a3c9d5639e
commit
1e59d5b735
@ -24,3 +24,16 @@ export enum EIssueCommentAccessSpecifier {
|
|||||||
EXTERNAL = "EXTERNAL",
|
EXTERNAL = "EXTERNAL",
|
||||||
INTERNAL = "INTERNAL",
|
INTERNAL = "INTERNAL",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// estimates
|
||||||
|
export enum EEstimateSystem {
|
||||||
|
POINTS = "points",
|
||||||
|
CATEGORIES = "categories",
|
||||||
|
TIME = "time",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EEstimateUpdateStages {
|
||||||
|
CREATE = "create",
|
||||||
|
EDIT = "edit",
|
||||||
|
SWITCH = "switch",
|
||||||
|
}
|
||||||
|
37
packages/types/src/estimate.d.ts
vendored
37
packages/types/src/estimate.d.ts
vendored
@ -1,3 +1,6 @@
|
|||||||
|
import { IWorkspace, IProject } from "./";
|
||||||
|
import { EEstimateSystem, EEstimateUpdateStages } from "./enums";
|
||||||
|
|
||||||
export interface IEstimatePoint {
|
export interface IEstimatePoint {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
key: number | undefined;
|
key: number | undefined;
|
||||||
@ -12,13 +15,16 @@ export interface IEstimatePoint {
|
|||||||
updated_by: string | undefined;
|
updated_by: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TEstimateType = "categories" | "points" | "time";
|
export type TEstimateSystemKeys =
|
||||||
|
| EEstimateSystem.POINTS
|
||||||
|
| EEstimateSystem.CATEGORIES
|
||||||
|
| EEstimateSystem.TIME;
|
||||||
|
|
||||||
export interface IEstimate {
|
export interface IEstimate {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
type: TEstimateType | undefined; // categories, points, time
|
type: TEstimateSystemKeys | undefined; // categories, points, time
|
||||||
points: IEstimatePoint[] | undefined;
|
points: IEstimatePoint[] | undefined;
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
workspace_detail: IWorkspace | undefined;
|
workspace_detail: IWorkspace | undefined;
|
||||||
@ -40,3 +46,30 @@ export interface IEstimateFormData {
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TEstimatePointsObject = {
|
||||||
|
id?: string | undefined;
|
||||||
|
key: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTemplateValues = {
|
||||||
|
title: string;
|
||||||
|
values: TEstimatePointsObject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEstimateSystem = {
|
||||||
|
name: string;
|
||||||
|
templates: Record<string, TTemplateValues>;
|
||||||
|
is_available: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEstimateSystems = {
|
||||||
|
[K in TEstimateSystemKeys]: TEstimateSystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
// update estimates
|
||||||
|
export type TEstimateUpdateStageKeys =
|
||||||
|
| EEstimateUpdateStages.CREATE
|
||||||
|
| EEstimateUpdateStages.EDIT
|
||||||
|
| EEstimateUpdateStages.SWITCH;
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { FC, useEffect, useMemo, useState } from "react";
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject } from "@plane/types";
|
||||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||||
import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/components/estimates";
|
import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/components/estimates";
|
||||||
import { TEstimateSystemKeys, EEstimateSystem, TEstimatePointsObject } from "@/components/estimates/types";
|
|
||||||
// constants
|
// constants
|
||||||
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectEstimates } from "@/hooks/store";
|
import { useProjectEstimates } from "@/hooks/store";
|
||||||
|
|
||||||
@ -18,7 +16,6 @@ type TCreateEstimateModal = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data?: IEstimate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) => {
|
export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) => {
|
||||||
@ -30,10 +27,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.POINTS);
|
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.POINTS);
|
||||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||||
|
|
||||||
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => {
|
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
|
||||||
const points = cloneDeep(newPoints);
|
|
||||||
setEstimatePoints(points);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -47,17 +41,21 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
|
|
||||||
const handleCreateEstimate = async () => {
|
const handleCreateEstimate = async () => {
|
||||||
try {
|
try {
|
||||||
|
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) => {
|
||||||
if (estimatePoint.value && Number(estimatePoint.value)) validatedEstimatePoints.push(estimatePoint);
|
if (
|
||||||
|
estimatePoint.value &&
|
||||||
|
((estimatePoint.value != "0" && Number(estimatePoint.value)) || estimatePoint.value === "0")
|
||||||
|
)
|
||||||
|
validatedEstimatePoints.push(estimatePoint);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
estimatePoints?.map((estimatePoint) => {
|
estimatePoints?.map((estimatePoint) => {
|
||||||
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: {
|
||||||
@ -65,8 +63,12 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
|
|||||||
},
|
},
|
||||||
estimate_points: validatedEstimatePoints,
|
estimate_points: validatedEstimatePoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createEstimate(workspaceSlug, projectId, payload);
|
await createEstimate(workspaceSlug, projectId, payload);
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: "Estimate system created",
|
||||||
|
message: "Created and Enabled successfully",
|
||||||
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
} else {
|
} else {
|
||||||
setToast({
|
setToast({
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// components
|
import { TEstimateSystemKeys } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { RadioInput } from "@plane/ui";
|
import { RadioInput } from "@plane/ui";
|
||||||
import { TEstimateSystemKeys } from "@/components/estimates/types";
|
|
||||||
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
import { TEstimatePointsObject } from "@plane/types";
|
||||||
import { Button, Sortable } from "@plane/ui";
|
import { Button, Sortable } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EstimateItem } from "@/components/estimates";
|
import { EstimatePointItem } from "@/components/estimates";
|
||||||
import { EEstimateSystem, TEstimatePointsObject } from "@/components/estimates/types";
|
|
||||||
// constants
|
// constants
|
||||||
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||||
|
|
||||||
type TEstimateCreateStageTwo = {
|
type TEstimateCreateStageTwo = {
|
||||||
estimateSystem: EEstimateSystem;
|
estimateSystem: EEstimateSystem;
|
||||||
@ -17,10 +17,10 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
|
|||||||
const { estimateSystem, estimatePoints, handleEstimatePoints } = props;
|
const { estimateSystem, estimatePoints, handleEstimatePoints } = props;
|
||||||
|
|
||||||
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined;
|
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined;
|
||||||
|
const maxEstimatesCount = 11;
|
||||||
|
|
||||||
const addNewEstimationPoint = () => {
|
const addNewEstimationPoint = () => {
|
||||||
const currentEstimationPoints = estimatePoints;
|
const currentEstimationPoints = estimatePoints;
|
||||||
|
|
||||||
const newEstimationPoint: TEstimatePointsObject = {
|
const newEstimationPoint: TEstimatePointsObject = {
|
||||||
key: currentEstimationPoints.length + 1,
|
key: currentEstimationPoints.length + 1,
|
||||||
value: "0",
|
value: "0",
|
||||||
@ -28,32 +28,53 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
|
|||||||
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
|
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEstimationPoint = (index: number) => {
|
const editEstimationPoint = (index: number, value: string) => {
|
||||||
const newEstimationPoints = estimatePoints;
|
const newEstimationPoints = estimatePoints;
|
||||||
newEstimationPoints.splice(index, 1);
|
newEstimationPoints[index].value = value;
|
||||||
handleEstimatePoints(newEstimationPoints);
|
handleEstimatePoints(newEstimationPoints);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) =>
|
const deleteEstimationPoint = (index: number) => {
|
||||||
updatedEstimatePoints.map((item, index) => ({
|
let newEstimationPoints = estimatePoints;
|
||||||
|
newEstimationPoints.splice(index, 1);
|
||||||
|
newEstimationPoints = newEstimationPoints.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
key: index + 1,
|
||||||
|
}));
|
||||||
|
handleEstimatePoints(newEstimationPoints);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) => {
|
||||||
|
const sortedEstimatePoints = updatedEstimatePoints.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
key: index + 1,
|
key: index + 1,
|
||||||
})) as TEstimatePointsObject[];
|
})) as TEstimatePointsObject[];
|
||||||
|
return sortedEstimatePoints;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-1">
|
||||||
<Sortable
|
<div className="text-sm font-medium text-custom-text-300">{estimateSystem}</div>
|
||||||
data={estimatePoints}
|
<div className="space-y-3">
|
||||||
render={(value: TEstimatePointsObject, index: number) => (
|
<Sortable
|
||||||
<EstimateItem item={value} deleteItem={() => deleteEstimationPoint(index)} />
|
data={estimatePoints}
|
||||||
|
render={(value: TEstimatePointsObject, index: number) => (
|
||||||
|
<EstimatePointItem
|
||||||
|
mode={EEstimateUpdateStages.CREATE}
|
||||||
|
item={value}
|
||||||
|
editItem={(value: string) => editEstimationPoint(index, value)}
|
||||||
|
deleteItem={() => deleteEstimationPoint(index)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
|
||||||
|
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||||
|
/>
|
||||||
|
{estimatePoints && estimatePoints.length <= maxEstimatesCount && (
|
||||||
|
<Button size="sm" prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
||||||
|
Add {currentEstimateSystem?.name}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
|
</div>
|
||||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
|
||||||
Add {currentEstimateSystem?.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from "./modal";
|
|
@ -1,24 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { IEstimate } from "@plane/types";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
data?: IEstimate;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
|
||||||
const { handleClose, isOpen, data } = props;
|
|
||||||
|
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
|
||||||
<div>Delete Estimate Modal</div>
|
|
||||||
</ModalCore>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,4 +1,5 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Pen } from "lucide-react";
|
import { Pen } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
@ -31,7 +32,11 @@ export const EstimateListItem: FC<TEstimateListItem> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="font-medium text-base">{currentEstimate?.name}</h3>
|
<h3 className="font-medium text-base">{currentEstimate?.name}</h3>
|
||||||
<p className="text-xs">{currentEstimate?.points?.map((estimatePoint) => estimatePoint?.value).join(", ")}</p>
|
<p className="text-xs">
|
||||||
|
{sortBy(currentEstimate?.points, ["key"])
|
||||||
|
?.map((estimatePoint) => estimatePoint?.value)
|
||||||
|
.join(", ")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && isEditable && (
|
{isAdmin && isEditable && (
|
||||||
<div
|
<div
|
||||||
|
@ -13,6 +13,9 @@ export * from "./estimate-list-item";
|
|||||||
// create
|
// create
|
||||||
export * from "./create";
|
export * from "./create";
|
||||||
|
|
||||||
|
// create
|
||||||
|
export * from "./update";
|
||||||
|
|
||||||
// estimate points
|
// estimate points
|
||||||
export * from "./points/estimate-point-item";
|
export * from "./points/estimate-point-item";
|
||||||
export * from "./points/inline-editable";
|
export * from "./points/inline-editable";
|
||||||
|
@ -1,20 +1,55 @@
|
|||||||
import { Fragment, useRef, useState } from "react";
|
import { FC, Fragment, useEffect, useRef, useState } from "react";
|
||||||
import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react";
|
import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react";
|
||||||
import { Select } from "@headlessui/react";
|
import { Select } from "@headlessui/react";
|
||||||
|
import { TEstimatePointsObject } from "@plane/types";
|
||||||
import { Draggable } from "@plane/ui";
|
import { Draggable } from "@plane/ui";
|
||||||
|
// constants
|
||||||
|
import { EEstimateUpdateStages } from "@/constants/estimates";
|
||||||
|
// components
|
||||||
import { InlineEdit } from "./inline-editable";
|
import { InlineEdit } from "./inline-editable";
|
||||||
import { TEstimatePointsObject } from "../types";
|
|
||||||
|
|
||||||
type Props = {
|
type TEstimatePointItem = {
|
||||||
|
mode: EEstimateUpdateStages;
|
||||||
item: TEstimatePointsObject;
|
item: TEstimatePointsObject;
|
||||||
|
editItem: (value: string) => void;
|
||||||
deleteItem: () => void;
|
deleteItem: () => void;
|
||||||
};
|
};
|
||||||
const EstimateItem = ({ item, deleteItem }: Props) => {
|
|
||||||
const { value, id } = item;
|
|
||||||
|
|
||||||
|
const EstimatePointItem: FC<TEstimatePointItem> = (props) => {
|
||||||
|
// props
|
||||||
|
const { mode, item, editItem, deleteItem } = props;
|
||||||
|
const { id, key, value } = item;
|
||||||
|
// ref
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [showDeleteUI, setShowDeleteUI] = useState(false);
|
// states
|
||||||
|
const [inputValue, setInputValue] = useState<string | undefined>(undefined);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [showDeleteUI, setShowDeleteUI] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputValue === undefined) setInputValue(value);
|
||||||
|
}, [value, inputValue]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (id) {
|
||||||
|
// Make the api call to save the estimate point
|
||||||
|
// Show a spinner
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (value: string) => {
|
||||||
|
if (id) {
|
||||||
|
setIsEditing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
inputRef.current?.select();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setInputValue(value);
|
||||||
|
editItem(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -24,30 +59,107 @@ const EstimateItem = ({ item, deleteItem }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
setIsEditing(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
inputRef.current?.select();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (id) {
|
|
||||||
// Make the api call to save the estimate point
|
|
||||||
// Show a spinner
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Draggable data={item}>
|
<Draggable data={item}>
|
||||||
{isEditing && (
|
{mode === EEstimateUpdateStages.CREATE && (
|
||||||
<div className="flex justify-between items-center gap-4 mb-2">
|
<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) => handleEdit(e.target.value)}
|
||||||
|
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
|
||||||
|
/>
|
||||||
|
<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="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-2">
|
||||||
|
<div className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer">
|
||||||
|
<GripVertical size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Pencil size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === EEstimateUpdateStages.SWITCH && (
|
||||||
|
<div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-2">
|
||||||
|
<div className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer">
|
||||||
|
<GripVertical size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Pencil size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="text-custom-text-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className="border border-custom-border-200 rounded relative flex items-center px-2.5 gap-3">
|
||||||
|
<GripVertical size={14} className="text-custom-text-200" />
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={() => {}}
|
||||||
|
className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5"
|
||||||
|
/>
|
||||||
|
<Pencil size={14} className="text-custom-text-200" />
|
||||||
|
<Trash2 size={14} className="text-custom-text-200" />
|
||||||
|
<Check size={14} className="text-custom-text-200" />
|
||||||
|
<X size={14} className="text-custom-text-200" />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* {isEditing && (
|
||||||
|
<div className="flex justify-between items-center gap-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
className="border rounded-md border-custom-border-300 p-3 flex-grow"
|
className="border rounded-md border-custom-border-300 p-3 flex-grow"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
@ -58,6 +170,7 @@ const EstimateItem = ({ item, deleteItem }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<div className="border rounded-md border-custom-border-300 mb-2 p-3 flex justify-between items-center">
|
<div className="border rounded-md border-custom-border-300 mb-2 p-3 flex justify-between items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -82,9 +195,9 @@ const EstimateItem = ({ item, deleteItem }: Props) => {
|
|||||||
{!showDeleteUI && <Trash2 className="w-4 h-4" onClick={handleDelete} />}
|
{!showDeleteUI && <Trash2 className="w-4 h-4" onClick={handleDelete} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { EstimateItem };
|
export { EstimatePointItem };
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { IEstimate } from "@plane/types";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
EstimateLoaderScreen,
|
EstimateLoaderScreen,
|
||||||
EstimateEmptyScreen,
|
EstimateEmptyScreen,
|
||||||
EstimateDisableSwitch,
|
EstimateDisableSwitch,
|
||||||
CreateEstimateModal,
|
CreateEstimateModal,
|
||||||
|
UpdateEstimateModal,
|
||||||
EstimateList,
|
EstimateList,
|
||||||
} from "@/components/estimates";
|
} from "@/components/estimates";
|
||||||
// hooks
|
// hooks
|
||||||
@ -22,29 +22,18 @@ type TEstimateRoot = {
|
|||||||
export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, isAdmin } = props;
|
const { workspaceSlug, projectId, isAdmin } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { loader, currentActiveEstimateId, estimateById, archivedEstimateIds, getProjectEstimates } =
|
const { loader, currentActiveEstimateId, archivedEstimateIds, getProjectEstimates } = useProjectEstimates();
|
||||||
useProjectEstimates();
|
|
||||||
// states
|
// states
|
||||||
const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(false);
|
const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(false);
|
||||||
// const [isEstimateDeleteModalOpen, setIsEstimateDeleteModalOpen] = useState<string | null>(null);
|
const [estimateToUpdate, setEstimateToUpdate] = useState<string | undefined>();
|
||||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
|
||||||
|
|
||||||
const { isLoading: isSWRLoading } = useSWR(
|
const { isLoading: isSWRLoading } = useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
||||||
async () => workspaceSlug && projectId && getProjectEstimates(workspaceSlug, projectId)
|
async () => workspaceSlug && projectId && getProjectEstimates(workspaceSlug, projectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const onEditClick = (estimateId: string) => {
|
|
||||||
const currentEstimate = estimateById(estimateId);
|
|
||||||
setEstimateToUpdate(currentEstimate);
|
|
||||||
setIsEstimateCreateModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
{/* <EstimateLoaderScreen />
|
|
||||||
<EstimateEmptyScreen onButtonClick={() => {}} /> */}
|
|
||||||
|
|
||||||
{loader === "init-loader" || isSWRLoading ? (
|
{loader === "init-loader" || isSWRLoading ? (
|
||||||
<EstimateLoaderScreen />
|
<EstimateLoaderScreen />
|
||||||
) : (
|
) : (
|
||||||
@ -71,7 +60,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
|||||||
estimateIds={[currentActiveEstimateId]}
|
estimateIds={[currentActiveEstimateId]}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
isEditable
|
isEditable
|
||||||
onEditClick={onEditClick}
|
onEditClick={(estimateId: string) => setEstimateToUpdate(estimateId)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -102,17 +91,21 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isOpen={isEstimateCreateModalOpen}
|
isOpen={isEstimateCreateModalOpen}
|
||||||
data={estimateToUpdate}
|
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
setIsEstimateCreateModalOpen(false);
|
setIsEstimateCreateModalOpen(false);
|
||||||
setEstimateToUpdate(undefined);
|
setEstimateToUpdate(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <DeleteEstimateModal
|
<UpdateEstimateModal
|
||||||
isOpen={!!isEstimateDeleteModalOpen}
|
workspaceSlug={workspaceSlug}
|
||||||
handleClose={() => setIsEstimateDeleteModalOpen(null)}
|
projectId={projectId}
|
||||||
data={}
|
estimateId={estimateToUpdate ? estimateToUpdate : undefined}
|
||||||
/> */}
|
isOpen={estimateToUpdate ? true : false}
|
||||||
|
handleClose={() => {
|
||||||
|
setIsEstimateCreateModalOpen(false);
|
||||||
|
setEstimateToUpdate(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
export enum EEstimateSystem {
|
|
||||||
POINTS = "points",
|
|
||||||
CATEGORIES = "categories",
|
|
||||||
TIME = "time",
|
|
||||||
}
|
|
||||||
export type TEstimateSystemKeys = EEstimateSystem.POINTS | EEstimateSystem.CATEGORIES | EEstimateSystem.TIME;
|
|
||||||
|
|
||||||
export type TEstimatePointsObject = { id?: string | undefined; key: number; value: string };
|
|
||||||
|
|
||||||
export type TTemplateValues = {
|
|
||||||
title: string;
|
|
||||||
values: TEstimatePointsObject[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TEstimateSystem = {
|
|
||||||
name: string;
|
|
||||||
templates: Record<string, TTemplateValues>;
|
|
||||||
is_available: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TEstimateSystems = {
|
|
||||||
[K in TEstimateSystemKeys]: TEstimateSystem;
|
|
||||||
};
|
|
@ -1 +1,3 @@
|
|||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
|
export * from "./stage-one";
|
||||||
|
export * from "./stage-two";
|
||||||
|
@ -1,47 +1,157 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { IEstimate } from "@plane/types";
|
import { ChevronLeft } from "lucide-react";
|
||||||
import { Button } from "@plane/ui";
|
import { IEstimateFormData, TEstimatePointsObject, TEstimateUpdateStageKeys, TEstimateSystemKeys } from "@plane/types";
|
||||||
|
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||||
// types
|
import { EstimateUpdateStageOne, EstimateUpdateStageTwo } from "@/components/estimates";
|
||||||
import { TEstimatePointsObject } from "@/components/estimates/types";
|
// constants
|
||||||
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
|
// hooks
|
||||||
|
import {
|
||||||
|
useEstimate,
|
||||||
|
// useProjectEstimates
|
||||||
|
} from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type TUpdateEstimateModal = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
estimateId: string | undefined;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data?: IEstimate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UpdateEstimateModal: FC<Props> = observer((props) => {
|
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { handleClose, isOpen } = props;
|
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
|
||||||
|
// hooks
|
||||||
|
const { asJson: currentEstimate, updateEstimate } = useEstimate(estimateId);
|
||||||
// states
|
// states
|
||||||
|
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
|
||||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
|
||||||
|
if (currentEstimate?.points && currentEstimate?.points.length > 0) {
|
||||||
|
const estimateValidatePoints: TEstimatePointsObject[] = [];
|
||||||
|
currentEstimate?.points.map(
|
||||||
|
(point) =>
|
||||||
|
point.key && point.value && estimateValidatePoints.push({ id: point.id, key: point.key, value: point.value })
|
||||||
|
);
|
||||||
|
if (estimateValidatePoints.length > 0) {
|
||||||
|
setEstimateEditType(type);
|
||||||
|
setEstimatePoints(estimateValidatePoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => {
|
||||||
|
const points = cloneDeep(newPoints);
|
||||||
|
setEstimatePoints(points);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
setEstimateEditType(undefined);
|
||||||
setEstimatePoints(undefined);
|
setEstimatePoints(undefined);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
|
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
|
||||||
|
|
||||||
|
const handleCreateEstimate = async () => {
|
||||||
|
try {
|
||||||
|
if (!workspaceSlug || !projectId || !estimateId || currentEstimate?.type === undefined) return;
|
||||||
|
const currentEstimationType: TEstimateSystemKeys = currentEstimate?.type;
|
||||||
|
const validatedEstimatePoints: TEstimatePointsObject[] = [];
|
||||||
|
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(currentEstimationType)) {
|
||||||
|
estimatePoints?.map((estimatePoint) => {
|
||||||
|
if (
|
||||||
|
estimatePoint.value &&
|
||||||
|
((estimatePoint.value != "0" && Number(estimatePoint.value)) || estimatePoint.value === "0")
|
||||||
|
)
|
||||||
|
validatedEstimatePoints.push(estimatePoint);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
estimatePoints?.map((estimatePoint) => {
|
||||||
|
if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (validatedEstimatePoints.length === estimatePoints?.length) {
|
||||||
|
const payload: IEstimateFormData = {
|
||||||
|
estimate: {
|
||||||
|
type: "points",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("estimateStage", estimateEditType);
|
||||||
|
|
||||||
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">
|
||||||
{/* heading */}
|
{/* heading */}
|
||||||
<div className="relative flex justify-between items-center gap-2 px-5">Heading</div>
|
<div className="relative flex justify-between items-center gap-2 px-5">
|
||||||
|
<div className="relative flex items-center gap-1">
|
||||||
|
{estimateEditType && (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setEstimateEditType(undefined);
|
||||||
|
handleUpdatePoints(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 className="text-xs text-gray-400">Step {renderEstimateStepsCount}/2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* estimate steps */}
|
{/* estimate steps */}
|
||||||
<div className="px-5">Content</div>
|
<div className="px-5">
|
||||||
|
{!estimateEditType && <EstimateUpdateStageOne handleEstimateEditType={handleEstimateEditType} />}
|
||||||
|
{estimateEditType && estimatePoints && (
|
||||||
|
<EstimateUpdateStageTwo
|
||||||
|
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">
|
<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}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{estimatePoints && (
|
{estimatePoints && (
|
||||||
<Button variant="primary" size="sm" onClick={handleClose}>
|
<Button variant="primary" size="sm" onClick={handleCreateEstimate}>
|
||||||
Create Estimate
|
Create Estimate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
32
web/components/estimates/update/stage-one.tsx
Normal file
32
web/components/estimates/update/stage-one.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { TEstimateUpdateStageKeys } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ESTIMATE_OPTIONS_STAGE_ONE } from "@/constants/estimates";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
|
type TEstimateUpdateStageOne = {
|
||||||
|
handleEstimateEditType: (stage: TEstimateUpdateStageKeys) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateUpdateStageOne: FC<TEstimateUpdateStageOne> = (props) => {
|
||||||
|
const { handleEstimateEditType } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{ESTIMATE_OPTIONS_STAGE_ONE &&
|
||||||
|
ESTIMATE_OPTIONS_STAGE_ONE.map((stage) => (
|
||||||
|
<div
|
||||||
|
key={stage.key}
|
||||||
|
className={cn(
|
||||||
|
"border border-custom-border-300 cursor-pointer space-y-1 p-3 rounded hover:bg-custom-background-90 transition-colors"
|
||||||
|
)}
|
||||||
|
onClick={() => handleEstimateEditType(stage.key)}
|
||||||
|
>
|
||||||
|
<h3 className="text-base font-medium">{stage.title}</h3>
|
||||||
|
<p className="text-sm text-custom-text-200">{stage.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
58
web/components/estimates/update/stage-two.tsx
Normal file
58
web/components/estimates/update/stage-two.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { FC } from "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";
|
||||||
|
|
||||||
|
type TEstimateUpdateStageTwo = {
|
||||||
|
estimate: IEstimate;
|
||||||
|
estimateEditType: TEstimateUpdateStageKeys | undefined;
|
||||||
|
estimatePoints: TEstimatePointsObject[];
|
||||||
|
handleEstimatePoints: (value: TEstimatePointsObject[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateUpdateStageTwo: FC<TEstimateUpdateStageTwo> = (props) => {
|
||||||
|
const { estimate, estimateEditType, estimatePoints, handleEstimatePoints } = props;
|
||||||
|
|
||||||
|
const currentEstimateSystem = estimate || undefined;
|
||||||
|
|
||||||
|
const addNewEstimationPoint = () => {
|
||||||
|
const currentEstimationPoints = estimatePoints;
|
||||||
|
|
||||||
|
const newEstimationPoint: TEstimatePointsObject = {
|
||||||
|
key: currentEstimationPoints.length + 1,
|
||||||
|
value: "0",
|
||||||
|
};
|
||||||
|
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEstimationPoint = (index: number) => {
|
||||||
|
const newEstimationPoints = estimatePoints;
|
||||||
|
newEstimationPoints.splice(index, 1);
|
||||||
|
handleEstimatePoints(newEstimationPoints);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) =>
|
||||||
|
updatedEstimatePoints.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
key: index + 1,
|
||||||
|
})) as TEstimatePointsObject[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Sortable
|
||||||
|
data={estimatePoints}
|
||||||
|
render={(value: TEstimatePointsObject, index: number) => (
|
||||||
|
<EstimatePointItem item={value} deleteItem={() => deleteEstimationPoint(index)} />
|
||||||
|
)}
|
||||||
|
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
|
||||||
|
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
||||||
|
Add {currentEstimateSystem?.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,17 @@
|
|||||||
import { TEstimateSystems } from "@/components/estimates/types";
|
export enum EEstimateSystem {
|
||||||
|
POINTS = "points",
|
||||||
|
CATEGORIES = "categories",
|
||||||
|
TIME = "time",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EEstimateUpdateStages {
|
||||||
|
CREATE = "create",
|
||||||
|
EDIT = "edit",
|
||||||
|
SWITCH = "switch",
|
||||||
|
}
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { TEstimateSystems } from "@plane/types";
|
||||||
|
|
||||||
export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
||||||
points: {
|
points: {
|
||||||
@ -93,3 +106,16 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
|||||||
is_available: false,
|
is_available: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ESTIMATE_OPTIONS_STAGE_ONE = [
|
||||||
|
{
|
||||||
|
key: EEstimateUpdateStages.EDIT,
|
||||||
|
title: "Add, update or remove estimates",
|
||||||
|
description: "Manage current system either adding, updating or removing the points or categories.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EEstimateUpdateStages.SWITCH,
|
||||||
|
title: "Change estimate type",
|
||||||
|
description: "Convert your points system to categories system and vice versa.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
IEstimatePoint as IEstimatePointType,
|
IEstimatePoint as IEstimatePointType,
|
||||||
IProject,
|
IProject,
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
TEstimateType,
|
TEstimateSystemKeys,
|
||||||
IEstimateFormData,
|
IEstimateFormData,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// services
|
// services
|
||||||
@ -31,8 +31,8 @@ export interface IEstimate extends IEstimateType {
|
|||||||
EstimatePointIds: string[] | undefined;
|
EstimatePointIds: string[] | undefined;
|
||||||
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
|
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
|
||||||
// actions
|
// actions
|
||||||
updateEstimatePointSorting: (payload: IEstimateFormData) => Promise<void>;
|
updateEstimate: (payload: IEstimateFormData) => Promise<void>;
|
||||||
deleteEstimatePoint: (estimatePointId: string) => Promise<void>;
|
deleteEstimate: (estimatePointId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Estimate implements IEstimate {
|
export class Estimate implements IEstimate {
|
||||||
@ -40,7 +40,7 @@ export class Estimate implements IEstimate {
|
|||||||
id: string | undefined = undefined;
|
id: string | undefined = undefined;
|
||||||
name: string | undefined = undefined;
|
name: string | undefined = undefined;
|
||||||
description: string | undefined = undefined;
|
description: string | undefined = undefined;
|
||||||
type: TEstimateType | undefined = undefined;
|
type: TEstimateSystemKeys | undefined = undefined;
|
||||||
points: IEstimatePointType[] | undefined = undefined;
|
points: IEstimatePointType[] | undefined = undefined;
|
||||||
workspace: string | undefined = undefined;
|
workspace: string | undefined = undefined;
|
||||||
workspace_detail: IWorkspace | undefined = undefined;
|
workspace_detail: IWorkspace | undefined = undefined;
|
||||||
@ -82,8 +82,8 @@ export class Estimate implements IEstimate {
|
|||||||
asJson: computed,
|
asJson: computed,
|
||||||
EstimatePointIds: computed,
|
EstimatePointIds: computed,
|
||||||
// actions
|
// actions
|
||||||
updateEstimatePointSorting: action,
|
updateEstimate: action,
|
||||||
deleteEstimatePoint: action,
|
deleteEstimate: action,
|
||||||
});
|
});
|
||||||
this.id = this.data.id;
|
this.id = this.data.id;
|
||||||
this.name = this.data.name;
|
this.name = this.data.name;
|
||||||
@ -143,7 +143,7 @@ export class Estimate implements IEstimate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
updateEstimatePointSorting = async (payload: IEstimateFormData) => {
|
updateEstimate = async (payload: IEstimateFormData) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceSlug, projectId } = this.store.router;
|
const { workspaceSlug, projectId } = this.store.router;
|
||||||
if (!workspaceSlug || !projectId || !this.id || !payload) return;
|
if (!workspaceSlug || !projectId || !this.id || !payload) return;
|
||||||
@ -159,7 +159,7 @@ export class Estimate implements IEstimate {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteEstimatePoint = async (estimatePointId: string) => {
|
deleteEstimate = async (estimatePointId: string) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceSlug, projectId } = this.store.router;
|
const { workspaceSlug, projectId } = this.store.router;
|
||||||
if (!workspaceSlug || !projectId || !estimatePointId) return;
|
if (!workspaceSlug || !projectId || !estimatePointId) return;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
import update from "lodash/update";
|
import update from "lodash/update";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
@ -253,7 +254,8 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
|||||||
|
|
||||||
const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload);
|
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
|
// FIXME: i am getting different response from the server and once backend changes remove the get request and uncomment the commented code
|
||||||
const estimates = await this.getProjectEstimates(workspaceSlug, projectId, "mutation-loader");
|
let estimates = await this.getProjectEstimates(workspaceSlug, projectId, "mutation-loader");
|
||||||
|
estimates = sortBy(estimates, "created_at");
|
||||||
if (estimates && estimates.length > 0)
|
if (estimates && estimates.length > 0)
|
||||||
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
|
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
|
||||||
estimate: estimates[estimates.length - 1].id,
|
estimate: estimates[estimates.length - 1].id,
|
||||||
|
Loading…
Reference in New Issue
Block a user