fix: updated and handled the estimate points

This commit is contained in:
guru_sainath 2024-06-03 17:06:34 +05:30
parent a725385190
commit 16fa9c9baa
14 changed files with 129 additions and 129 deletions

View File

@ -63,7 +63,7 @@ export type TEstimateSystem = {
name: string;
templates: Record<string, TTemplateValues>;
is_available: boolean;
is_active: boolean;
is_ee: boolean;
};
export type TEstimateSystems = {

View File

@ -26,6 +26,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
// states
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.CATEGORIES);
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
const [buttonLoader, setButtonLoader] = useState(false);
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
@ -39,7 +40,7 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
const handleCreateEstimate = async () => {
try {
if (!workspaceSlug || !projectId || !estimatePoints) return;
setButtonLoader(true);
const payload: IEstimateFormData = {
estimate: {
name: ESTIMATE_SYSTEMS[estimateSystem]?.name,
@ -50,13 +51,15 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
};
await createEstimate(workspaceSlug, projectId, payload);
setButtonLoader(false);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Estimate created",
message: "Created and Enabled successfully",
message: "A new estimate has been added in your project.",
});
handleClose();
} catch (error) {
setButtonLoader(false);
setToast({
type: TOAST_TYPE.ERROR,
title: "Estimate creation failed",
@ -116,12 +119,12 @@ export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) =>
</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}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} disabled={buttonLoader}>
Cancel
</Button>
{estimatePoints && (
<Button variant="primary" size="sm" onClick={handleCreateEstimate}>
Create Estimate
<Button variant="primary" size="sm" onClick={handleCreateEstimate} disabled={buttonLoader}>
{buttonLoader ? `Creating` : `Create Estimate`}
</Button>
)}
</div>

View File

@ -31,7 +31,7 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
<Info size={12} />
</Tooltip>
</div>
) : ESTIMATE_SYSTEMS[currentSystem]?.is_active ? (
) : ESTIMATE_SYSTEMS[currentSystem]?.is_ee ? (
<div className="relative flex items-center gap-2 cursor-no-drop text-custom-text-300">
{ESTIMATE_SYSTEMS[currentSystem]?.name}
<Tooltip tooltipContent={"upgrade"}>
@ -42,7 +42,7 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
<div>{ESTIMATE_SYSTEMS[currentSystem]?.name}</div>
),
value: system,
disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available || ESTIMATE_SYSTEMS[currentSystem]?.is_active,
disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available || ESTIMATE_SYSTEMS[currentSystem]?.is_ee,
};
})}
label="Choose an estimate system"
@ -55,7 +55,7 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
/>
</div>
{ESTIMATE_SYSTEMS[estimateSystem]?.is_available && !ESTIMATE_SYSTEMS[estimateSystem]?.is_active && (
{ESTIMATE_SYSTEMS[estimateSystem]?.is_available && !ESTIMATE_SYSTEMS[estimateSystem]?.is_ee && (
<>
<div className="space-y-1.5">
<div className="text-sm font-medium text-custom-text-200">Start from scratch</div>

View File

@ -1,129 +1,79 @@
import { FC, useEffect, useMemo, useState } from "react";
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { ChevronLeft } from "lucide-react";
import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { EstimateCreateStageOne, EstimatePointCreateRoot } from "@/components/estimates";
// constants
import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/constants/estimates";
// hooks
import { useProjectEstimates } from "@/hooks/store";
import { useEstimate, useProject, useProjectEstimates } from "@/hooks/store";
type TDeleteEstimateModal = {
workspaceSlug: string;
projectId: string;
estimateId: string | undefined;
isOpen: boolean;
handleClose: () => void;
};
export const DeleteEstimateModal: FC<TDeleteEstimateModal> = observer((props) => {
// props
const { workspaceSlug, projectId, isOpen, handleClose } = props;
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
// hooks
const { createEstimate } = useProjectEstimates();
const { areEstimateEnabledByProjectId, deleteEstimate } = useProjectEstimates();
const { asJson: estimate } = useEstimate(estimateId);
const { updateProject } = useProject();
// states
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.CATEGORIES);
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
const [buttonLoader, setButtonLoader] = useState(false);
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => setEstimatePoints(newPoints);
useEffect(() => {
if (isOpen) {
setEstimateSystem(EEstimateSystem.CATEGORIES);
setEstimatePoints(undefined);
}
}, [isOpen]);
const handleCreateEstimate = async () => {
const handleDeleteEstimate = async () => {
try {
if (!workspaceSlug || !projectId || !estimatePoints) return;
const payload: IEstimateFormData = {
estimate: {
name: ESTIMATE_SYSTEMS[estimateSystem]?.name,
type: estimateSystem,
last_used: true,
},
estimate_points: estimatePoints,
};
await createEstimate(workspaceSlug, projectId, payload);
if (!workspaceSlug || !projectId || !estimateId) return;
setButtonLoader(true);
await deleteEstimate(workspaceSlug, projectId, estimateId);
if (areEstimateEnabledByProjectId(projectId)) {
await updateProject(workspaceSlug, projectId, { estimate: null });
}
setButtonLoader(false);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Estimate created",
message: "Created and Enabled successfully",
title: "Estimate deleted",
message: "Estimate has been removed from your project.",
});
handleClose();
} catch (error) {
setButtonLoader(false);
setToast({
type: TOAST_TYPE.ERROR,
title: "Estimate creation failed",
message: "We were unable to create the new estimate, please try again.",
message: "We were unable to delete the estimate, please try again.",
});
}
};
// derived values
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">
{/* heading */}
<div className="relative flex justify-between items-center gap-2 px-5">
<div className="relative flex items-center gap-1">
{estimatePoints && (
<div
onClick={() => {
setEstimateSystem(EEstimateSystem.CATEGORIES);
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-100">New Estimate System</div>
</div>
<div className="text-xs text-gray-400">Step {renderEstimateStepsCount} of 2</div>
<div className="text-xl font-medium text-custom-text-100">Delete Estimate System</div>
</div>
{/* estimate steps */}
<div className="px-5">
{!estimatePoints && (
<EstimateCreateStageOne
estimateSystem={estimateSystem}
handleEstimateSystem={setEstimateSystem}
handleEstimatePoints={(templateType: string) =>
handleUpdatePoints(ESTIMATE_SYSTEMS[estimateSystem].templates[templateType].values)
}
/>
)}
{estimatePoints && (
<>
<EstimatePointCreateRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={undefined}
estimateType={estimateSystem}
estimatePoints={estimatePoints}
setEstimatePoints={setEstimatePoints}
/>
</>
)}
<div className="text-base">
Deleting the estimate <span className="font-medium text-custom-primary-100">{estimate?.name}</span>
&nbsp;system will remove it from all issues permanently. This action cannot be undone. If you add estimates
again, you will need to update all the issues.
</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}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} disabled={buttonLoader}>
Cancel
</Button>
{estimatePoints && (
<Button variant="primary" size="sm" onClick={handleCreateEstimate}>
Create Estimate
</Button>
)}
<Button variant="danger" size="sm" onClick={handleDeleteEstimate} disabled={buttonLoader}>
{buttonLoader ? "Deleting" : "Delete Estimate"}
</Button>
</div>
</div>
</ModalCore>

View File

@ -0,0 +1,25 @@
import { FC } from "react";
import { Button } from "@plane/ui";
export const EstimateEEBanner: FC = (props) => {
const {} = props;
return (
<div className="border border-red-500 rounded overflow-hidden relative flex items-center">
<div>
<div className="text-lg">Estimate issues better with points</div>
<div className="text-base text-custom-text-200">
Use points to estimate scope of work better, monitor capacity, track the burn-down report for your project.
</div>
<div>
<Button variant="primary" size="sm">
Upgrade
</Button>
<div>Talk custom pricing</div>
</div>
</div>
<div className="border border-red-500 w-full">Image</div>
</div>
);
};

View File

@ -1,3 +1,4 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Pen, Trash } from "lucide-react";
@ -7,12 +8,15 @@ type TEstimateListItem = {
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;
return isAdmin && isEditable ? (
<div className="flex">
const { estimateId, isAdmin, isEditable, onEditClick, onDeleteClick } = 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)}
@ -21,10 +25,10 @@ export const EstimateListItemButtons: FC<TEstimateListItem> = observer((props) =
</button>
<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)}
onClick={() => onDeleteClick && onDeleteClick(estimateId)}
>
<Trash size={12} />
</button>
</div>
) : null;
);
});

View File

@ -1,6 +1,5 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Pen } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@ -13,10 +12,11 @@ type TEstimateListItem = {
isEstimateEnabled: boolean;
isEditable: boolean;
onEditClick?: (estimateId: string) => void;
onDeleteClick?: (estimateId: string) => void;
};
export const EstimateListItem: FC<TEstimateListItem> = observer((props) => {
const { estimateId, isAdmin, isEstimateEnabled, isEditable, onEditClick } = props;
const { estimateId, isAdmin, isEstimateEnabled, isEditable } = props;
// hooks
const { estimateById } = useProjectEstimates();
const { estimatePointIds, estimatePointById } = useEstimate(estimateId);

View File

@ -9,10 +9,11 @@ type TEstimateList = {
isEstimateEnabled?: boolean;
isEditable?: boolean;
onEditClick?: (estimateId: string) => void;
onDeleteClick?: (estimateId: string) => void;
};
export const EstimateList: FC<TEstimateList> = observer((props) => {
const { estimateIds, isAdmin, isEstimateEnabled = false, isEditable = false, onEditClick } = props;
const { estimateIds, isAdmin, isEstimateEnabled = false, isEditable = false, onEditClick, onDeleteClick } = props;
if (!estimateIds || estimateIds?.length <= 0) return <></>;
return (
@ -26,6 +27,7 @@ export const EstimateList: FC<TEstimateList> = observer((props) => {
isEstimateEnabled={isEstimateEnabled}
isEditable={isEditable}
onEditClick={onEditClick}
onDeleteClick={onDeleteClick}
/>
))}
</div>

View File

@ -2,6 +2,7 @@ export * from "./root";
export * from "./empty-screen";
export * from "./loader-screen";
export * from "./ee-banner";
export * from "./estimate-search";
export * from "./estimate-disable-switch";
@ -9,11 +10,12 @@ export * from "./estimate-disable-switch";
// estimates
export * from "./estimate-list";
export * from "./estimate-list-item";
export * from "./estimate-list-item-buttons";
// create
export * from "./create";
// create
// update
export * from "./update";
// delete
@ -21,5 +23,3 @@ export * from "./delete";
// estimate points
export * from "./points";
export * from "./estimate-list-item-buttons";

View File

@ -8,7 +8,9 @@ import {
EstimateDisableSwitch,
CreateEstimateModal,
UpdateEstimateModal,
DeleteEstimateModal,
EstimateList,
EstimateEEBanner,
} from "@/components/estimates";
// hooks
import { useProject, useProjectEstimates } from "@/hooks/store";
@ -27,6 +29,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
// states
const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(false);
const [estimateToUpdate, setEstimateToUpdate] = useState<string | undefined>();
const [estimateToDelete, setEstimateToDelete] = useState<string | undefined>();
const { isLoading: isSWRLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
@ -38,7 +41,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
{loader === "init-loader" || isSWRLoading ? (
<EstimateLoaderScreen />
) : (
<div className="space-y-10">
<div className="space-y-12">
{/* header */}
<div className="text-xl font-medium text-custom-text-100 border-b border-custom-border-200 py-3.5">
Estimates
@ -46,7 +49,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
{/* current active estimate section */}
{currentActiveEstimateId ? (
<div className="space-y-4">
<div className="">
{/* estimates activated deactivated section */}
<div className="relative border-b border-custom-border-200 pb-4 flex justify-between items-center gap-3">
<div className="space-y-1">
@ -64,6 +67,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
isEstimateEnabled={Boolean(currentProjectDetails?.estimate)}
isEditable
onEditClick={(estimateId: string) => setEstimateToUpdate(estimateId)}
onDeleteClick={(estimateId: string) => setEstimateToDelete(estimateId)}
/>
</div>
) : (
@ -72,7 +76,7 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
{/* archived estimates section */}
{archivedEstimateIds && archivedEstimateIds.length > 0 && (
<div>
<div className="">
<div className="border-b border-custom-border-200 space-y-1 pb-4">
<h3 className="text-lg font-medium text-custom-text-100">Archived estimates</h3>
<p className="text-sm text-custom-text-200">
@ -89,25 +93,28 @@ export const EstimateRoot: FC<TEstimateRoot> = observer((props) => {
</div>
)}
{/* <EstimateEEBanner /> */}
{/* CRUD modals */}
<CreateEstimateModal
workspaceSlug={workspaceSlug}
projectId={projectId}
isOpen={isEstimateCreateModalOpen}
handleClose={() => {
setIsEstimateCreateModalOpen(false);
setEstimateToUpdate(undefined);
}}
handleClose={() => setIsEstimateCreateModalOpen(false)}
/>
<UpdateEstimateModal
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={estimateToUpdate ? estimateToUpdate : undefined}
isOpen={estimateToUpdate ? true : false}
handleClose={() => {
setIsEstimateCreateModalOpen(false);
setEstimateToUpdate(undefined);
}}
handleClose={() => setEstimateToUpdate(undefined)}
/>
<DeleteEstimateModal
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={estimateToDelete ? estimateToDelete : undefined}
isOpen={estimateToDelete ? true : false}
handleClose={() => setEstimateToDelete(undefined)}
/>
</div>
);

View File

@ -22,13 +22,13 @@ export const EstimateUpdateStageOne: FC<TEstimateUpdateStageOne> = (props) => {
key={stage.key}
className={cn(
"border border-custom-border-300 cursor-pointer space-y-1 p-3 rounded transition-colors",
stage?.is_active ? `bg-custom-background-90` : `hover:bg-custom-background-90`
stage?.is_ee ? `bg-custom-background-90` : `hover:bg-custom-background-90`
)}
onClick={() => !stage?.is_active && handleEstimateEditType(stage.key)}
onClick={() => !stage?.is_ee && handleEstimateEditType(stage.key)}
>
<h3 className="text-base font-medium relative flex items-center gap-2">
{stage.title}
{stage?.is_active && (
{stage?.is_ee && (
<Tooltip tooltipContent={"upgrade"}>
<Crown size={12} className="text-amber-400" />
</Tooltip>

View File

@ -49,7 +49,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
},
is_available: true,
is_active: false,
is_ee: false,
},
points: {
name: "Points",
@ -102,7 +102,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
},
is_available: true,
is_active: true,
is_ee: false,
},
time: {
name: "Time",
@ -124,7 +124,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
},
is_available: false,
is_active: true,
is_ee: true,
},
};
@ -133,12 +133,12 @@ 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.",
is_active: false,
is_ee: true,
},
{
key: EEstimateUpdateStages.SWITCH,
title: "Change estimate type",
description: "Convert your points system to categories system and vice versa.",
is_active: true,
is_ee: true,
},
];

View File

@ -73,7 +73,7 @@ class EstimateService extends APIService {
}
}
async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise<any> {
async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise<void> {
try {
await this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`);
} catch (error) {

View File

@ -1,5 +1,6 @@
import orderBy from "lodash/orderBy";
import set from "lodash/set";
import unset from "lodash/unset";
import update from "lodash/update";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
@ -40,6 +41,7 @@ export interface IProjectEstimateStore {
projectId: string,
data: IEstimateFormData
) => Promise<IEstimateType | undefined>;
deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise<void>;
}
export class ProjectEstimateStore implements IProjectEstimateStore {
@ -62,6 +64,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
getProjectEstimates: action,
getEstimateById: action,
createEstimate: action,
deleteEstimate: action,
});
}
@ -249,9 +252,10 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
const estimate = await estimateService.createEstimate(workspaceSlug, projectId, payload);
if (estimate) {
await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
estimate: estimate.id,
});
// update estimate_id in current project
// await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, {
// estimate: estimate.id,
// });
runInAction(() => {
if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
});
@ -268,16 +272,21 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
};
/**
* @description deletes the given estimate for the given project
* @description delete the estimate for a project
* @param workspaceSlug
* @param projectId
* @param estimateId
*/
deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => {
await estimateService.deleteEstimate(workspaceSlug, projectId, estimateId).then(() => {
runInAction(() => {
delete this.estimates[estimateId];
});
});
try {
await estimateService.deleteEstimate(workspaceSlug, projectId, estimateId);
runInAction(() => estimateId && unset(this.estimates, [estimateId]));
} catch (error) {
this.error = {
status: "error",
message: "Error deleting estimate",
};
throw error;
}
};
}