chore: updated switch estimate

This commit is contained in:
guru_sainath 2024-05-29 13:01:59 +05:30
parent 39482b72ab
commit 9c279a62e0
9 changed files with 187 additions and 90 deletions

View File

@ -11,10 +11,6 @@ from rest_framework import serializers
class EstimateSerializer(BaseSerializer): class EstimateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta: class Meta:
model = Estimate model = Estimate

View File

@ -122,7 +122,12 @@ class BulkEstimatePointEndpoint(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
_ = Estimate.objects.get(pk=estimate_id) estimate = Estimate.objects.get(pk=estimate_id)
if request.data.get("estimate"):
estimate.name = request.data.get("estimate").get("name", estimate.name)
estimate.type = request.data.get("estimate").get("type", estimate.type)
estimate.save()
estimate_points_data = request.data.get("estimate_points", []) estimate_points_data = request.data.get("estimate_points", [])
@ -159,13 +164,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
batch_size=10, batch_size=10,
) )
estimate_point_serializer = EstimatePointSerializer( estimate_serializer = EstimateReadSerializer(estimate)
estimate_points, many=True
)
return Response( return Response(
{ estimate_serializer.data,
"points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View File

@ -1,11 +1,10 @@
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
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useProjectEstimates } from "@/hooks/store"; import { useEstimate, useProjectEstimates } from "@/hooks/store";
type TEstimateListItem = { type TEstimateListItem = {
estimateId: string; estimateId: string;
@ -19,11 +18,16 @@ export const EstimateListItem: FC<TEstimateListItem> = observer((props) => {
const { estimateId, isAdmin, isEstimateEnabled, isEditable, onEditClick } = props; const { estimateId, isAdmin, isEstimateEnabled, isEditable, onEditClick } = props;
// hooks // hooks
const { estimateById } = useProjectEstimates(); const { estimateById } = useProjectEstimates();
const { estimatePointIds, estimatePointById } = useEstimate(estimateId);
const currentEstimate = estimateById(estimateId); const currentEstimate = estimateById(estimateId);
if (!currentEstimate) return <></>; // derived values
const estimatePointValues = estimatePointIds?.map((estimatePointId) => {
const estimatePoint = estimatePointById(estimatePointId);
if (estimatePoint) return estimatePoint.value;
});
if (!currentEstimate) return <></>;
return ( return (
<div <div
className={cn( className={cn(
@ -33,11 +37,7 @@ 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"> <p className="text-xs">{(estimatePointValues || [])?.join(", ")}</p>
{sortBy(currentEstimate?.points, ["key"])
?.map((estimatePoint) => estimatePoint?.value)
.join(", ")}
</p>
</div> </div>
{isAdmin && isEditable && ( {isAdmin && isEditable && (
<div <div

View File

@ -19,7 +19,7 @@ type TEstimatePointDelete = {
export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) => { export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) => {
const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props; const { workspaceSlug, projectId, estimateId, estimatePointId, callback } = props;
// hooks // hooks
const { asJson: estimate, deleteEstimatePoint } = useEstimate(estimateId); const { estimatePointIds, estimatePointById, deleteEstimatePoint } = useEstimate(estimateId);
const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId); const { asJson: estimatePoint } = useEstimatePoint(estimateId, estimatePointId);
// states // states
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
@ -47,8 +47,12 @@ export const EstimatePointDelete: FC<TEstimatePointDelete> = observer((props) =>
}; };
// derived values // derived values
const selectDropdownOptions = const selectDropdownOptionIds = estimatePointIds?.filter((pointId) => pointId != estimatePointId) as string[];
estimate && estimate?.points ? estimate?.points.filter((point) => point.id !== estimatePointId) : []; const selectDropdownOptions = (selectDropdownOptionIds || [])?.map((pointId) => {
const estimatePoint = estimatePointById(pointId);
if (estimatePoint && estimatePoint?.id)
return { id: estimatePoint.id, key: estimatePoint.key, value: estimatePoint.value };
});
return ( return (
<div className="relative flex items-center gap-2 text-base"> <div className="relative flex items-center gap-2 text-base">

View File

@ -1,25 +1,28 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TEstimatePointsObject } from "@plane/types"; import { IEstimateFormData, TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { EstimatePointItemSwitchPreview } from "@/components/estimates/points"; import { EstimatePointItemSwitchPreview } from "@/components/estimates/points";
// constants // constants
import { EEstimateSystem, EEstimateUpdateStages } from "@/constants/estimates"; import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS } from "@/constants/estimates";
// hooks // hooks
import { useEstimate } from "@/hooks/store"; import { useEstimate } from "@/hooks/store";
type TEstimatePointSwitchRoot = { type TEstimatePointSwitchRoot = {
estimateSystemSwitchType: TEstimateSystemKeys;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
estimateId: string; estimateId: string;
handleClose: () => void;
mode?: EEstimateUpdateStages; mode?: EEstimateUpdateStages;
}; };
export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((props) => { export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((props) => {
// props // props
const { workspaceSlug, projectId, estimateId } = props; const { estimateSystemSwitchType, workspaceSlug, projectId, estimateId, handleClose } = props;
// hooks // hooks
const { asJson: estimate, estimatePointIds, estimatePointById } = useEstimate(estimateId); const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSwitch } = useEstimate(estimateId);
// states // states
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined); const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
@ -41,15 +44,62 @@ export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((p
}); });
}; };
const handleSwitchEstimate = async () => {
try {
if (!workspaceSlug || !projectId) return;
const validatedEstimatePoints: TEstimatePointsObject[] = [];
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystemSwitchType)) {
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: {
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();
} 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",
});
}
};
if (!workspaceSlug || !projectId || !estimateId || !estimatePoints) return <></>; if (!workspaceSlug || !projectId || !estimateId || !estimatePoints) return <></>;
return ( return (
<>
<div className="space-y-3"> <div className="space-y-3">
<div className="text-sm font-medium flex items-center gap-2"> <div className="text-sm font-medium flex items-center gap-2">
<div className="w-full">Current {estimate?.type}</div> <div className="w-full">Current {estimate?.type}</div>
<div className="flex-shrink-0 w-4 h-4" /> <div className="flex-shrink-0 w-4 h-4" />
<div className="w-full"> <div className="w-full">New {estimateSystemSwitchType}</div>
New {estimate?.type === EEstimateSystem?.POINTS ? EEstimateSystem?.CATEGORIES : EEstimateSystem?.POINTS}
</div>
</div> </div>
{estimatePoints.map((estimateObject, index) => ( {estimatePoints.map((estimateObject, index) => (
@ -62,5 +112,16 @@ export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((p
/> />
))} ))}
</div> </div>
<div className="relative flex justify-end items-center gap-3 px-5 pt-5 border-t border-custom-border-100">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="sm" onClick={handleSwitchEstimate}>
Update
</Button>
</div>
</>
); );
}); });

View File

@ -1,13 +1,15 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import { TEstimateUpdateStageKeys } from "@plane/types"; import { TEstimateSystemKeys, TEstimateUpdateStageKeys } from "@plane/types";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { EstimateUpdateStageOne, EstimatePointEditRoot, EstimatePointSwitchRoot } from "@/components/estimates"; import { EstimateUpdateStageOne, EstimatePointEditRoot, EstimatePointSwitchRoot } from "@/components/estimates";
// constants // constants
import { EEstimateUpdateStages } from "@/constants/estimates"; import { EEstimateSystem, EEstimateUpdateStages } from "@/constants/estimates";
// hooks
import { useEstimate } from "@/hooks/store";
type TUpdateEstimateModal = { type TUpdateEstimateModal = {
workspaceSlug: string; workspaceSlug: string;
@ -20,16 +22,26 @@ type TUpdateEstimateModal = {
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => { export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
// props // props
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props; const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
// hooks
const { asJson: estimate } = useEstimate(estimateId);
// states // states
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined); const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
const [estimateSystemSwitchType, setEstimateSystemSwitchType] = useState<TEstimateSystemKeys | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!isOpen) setEstimateEditType(undefined); if (!isOpen) {
setEstimateEditType(undefined);
setEstimateSystemSwitchType(undefined);
}
}, [isOpen]); }, [isOpen]);
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => setEstimateEditType(type); const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
if (type === EEstimateUpdateStages.SWITCH && estimate?.type)
const handleSwitchEstimate = () => {}; setEstimateSystemSwitchType(
estimate?.type === EEstimateSystem.CATEGORIES ? EEstimateSystem.POINTS : EEstimateSystem.CATEGORIES
);
setEstimateEditType(type);
};
return ( return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
@ -62,23 +74,24 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
{estimateEditType === EEstimateUpdateStages.EDIT && ( {estimateEditType === EEstimateUpdateStages.EDIT && (
<EstimatePointEditRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} /> <EstimatePointEditRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} />
)} )}
{estimateEditType === EEstimateUpdateStages.SWITCH && ( {estimateEditType === EEstimateUpdateStages.SWITCH && estimateSystemSwitchType && (
<EstimatePointSwitchRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} /> <EstimatePointSwitchRoot
estimateSystemSwitchType={estimateSystemSwitchType}
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={estimateId}
handleClose={handleClose}
/>
)} )}
</> </>
)} )}
</div> </div>
{[EEstimateUpdateStages.SWITCH, undefined].includes(estimateEditType) && ( {estimateEditType === undefined && (
<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>
{estimateEditType === EEstimateUpdateStages.SWITCH && (
<Button variant="primary" size="sm" onClick={handleSwitchEstimate}>
Update
</Button>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -61,7 +61,7 @@ export class EstimateService extends APIService {
projectId: string, projectId: string,
estimateId: string, estimateId: string,
payload: Partial<IEstimateFormData> payload: Partial<IEstimateFormData>
): Promise<{ points: IEstimatePoint[] } | undefined> { ): Promise<IEstimate | undefined> {
try { try {
const { data } = await this.patch( const { data } = await this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,

View File

@ -16,6 +16,8 @@ export interface IEstimatePoint extends IEstimatePointType {
error: TErrorCodes | undefined; error: TErrorCodes | undefined;
// computed // computed
asJson: IEstimatePointType; asJson: IEstimatePointType;
// helper actions
updateEstimatePointObject: (estimatePoint: Partial<IEstimatePointType>) => void;
// actions // actions
updateEstimatePoint: ( updateEstimatePoint: (
workspaceSlug: string, workspaceSlug: string,
@ -99,6 +101,14 @@ export class EstimatePoint implements IEstimatePoint {
}; };
} }
// helper actions
updateEstimatePointObject = (estimatePoint: Partial<IEstimatePointType>) => {
Object.keys(estimatePoint).map((key) => {
const estimatePointKey = key as keyof IEstimatePointType;
set(this, estimatePointKey, estimatePoint[estimatePointKey]);
});
};
// actions // actions
/** /**
* @description updating an estimate point * @description updating an estimate point

View File

@ -21,25 +21,25 @@ type TErrorCodes = {
message?: string; message?: string;
}; };
export interface IEstimate extends IEstimateType { export interface IEstimate extends Omit<IEstimateType, "points"> {
// observables // observables
error: TErrorCodes | undefined; error: TErrorCodes | undefined;
estimatePoints: Record<string, IEstimatePoint>; estimatePoints: Record<string, IEstimatePoint>;
// computed // computed
asJson: IEstimateType; asJson: Omit<IEstimateType, "points">;
estimatePointIds: string[] | undefined; estimatePointIds: string[] | undefined;
estimatePointById: (estimatePointId: string) => IEstimatePointType | undefined; estimatePointById: (estimatePointId: string) => IEstimatePointType | undefined;
// actions // actions
updateEstimate: (
workspaceSlug: string,
projectId: string,
payload: Partial<IEstimateFormData>
) => Promise<IEstimateType | undefined>;
updateEstimateSortOrder: ( updateEstimateSortOrder: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
payload: TEstimatePointsObject[] payload: TEstimatePointsObject[]
) => Promise<void>; ) => Promise<IEstimateType | undefined>;
updateEstimateSwitch: (
workspaceSlug: string,
projectId: string,
payload: IEstimateFormData
) => Promise<IEstimateType | undefined>;
creteEstimatePoint: ( creteEstimatePoint: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -59,7 +59,6 @@ export class Estimate implements IEstimate {
name: string | undefined = undefined; name: string | undefined = undefined;
description: string | undefined = undefined; description: string | undefined = undefined;
type: TEstimateSystemKeys | undefined = undefined; type: TEstimateSystemKeys | undefined = undefined;
points: IEstimatePointType[] | undefined = undefined;
workspace: string | undefined = undefined; workspace: string | undefined = undefined;
project: string | undefined = undefined; project: string | undefined = undefined;
last_used: boolean | undefined = undefined; last_used: boolean | undefined = undefined;
@ -83,7 +82,6 @@ export class Estimate implements IEstimate {
name: observable.ref, name: observable.ref,
description: observable.ref, description: observable.ref,
type: observable.ref, type: observable.ref,
points: observable,
workspace: observable.ref, workspace: observable.ref,
project: observable.ref, project: observable.ref,
last_used: observable.ref, last_used: observable.ref,
@ -98,8 +96,8 @@ export class Estimate implements IEstimate {
asJson: computed, asJson: computed,
estimatePointIds: computed, estimatePointIds: computed,
// actions // actions
updateEstimate: action,
updateEstimateSortOrder: action, updateEstimateSortOrder: action,
updateEstimateSwitch: action,
creteEstimatePoint: action, creteEstimatePoint: action,
deleteEstimatePoint: action, deleteEstimatePoint: action,
}); });
@ -107,7 +105,6 @@ export class Estimate implements IEstimate {
this.name = this.data.name; this.name = this.data.name;
this.description = this.data.description; this.description = this.data.description;
this.type = this.data.type; this.type = this.data.type;
this.points = this.data.points;
this.workspace = this.data.workspace; this.workspace = this.data.workspace;
this.project = this.data.project; this.project = this.data.project;
this.last_used = this.data.last_used; this.last_used = this.data.last_used;
@ -130,7 +127,6 @@ export class Estimate implements IEstimate {
name: this.name, name: this.name,
description: this.description, description: this.description,
type: this.type, type: this.type,
points: this.points,
workspace: this.workspace, workspace: this.workspace,
project: this.project, project: this.project,
last_used: this.last_used, last_used: this.last_used,
@ -159,22 +155,32 @@ export class Estimate implements IEstimate {
// actions // actions
/** /**
* @description update an estimate * @description update an estimate sort order
* @param { string } workspaceSlug * @param { string } workspaceSlug
* @param { string } projectId * @param { string } projectId
* @param { Partial<IEstimateFormData> } payload * @param { TEstimatePointsObject[] } payload
* @returns { IEstimateType | undefined } * @returns { IEstimateType | undefined }
*/ */
updateEstimate = async ( updateEstimateSortOrder = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
payload: Partial<IEstimateFormData> payload: TEstimatePointsObject[]
): Promise<IEstimateType | undefined> => { ): Promise<IEstimateType | undefined> => {
try { try {
if (!this.id || !payload) return; if (!this.id || !payload) return;
const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload); const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, {
return estimate as any; estimate_points: payload,
});
runInAction(() => {
estimate?.points &&
estimate?.points.map((estimatePoint) => {
if (estimatePoint.id)
set(this.estimatePoints, [estimatePoint.id], new EstimatePoint(this.store, this.data, estimatePoint));
});
});
return estimate;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -184,28 +190,34 @@ export class Estimate implements IEstimate {
* @description update an estimate sort order * @description update an estimate sort order
* @param { string } workspaceSlug * @param { string } workspaceSlug
* @param { string } projectId * @param { string } projectId
* @param { Partial<IEstimateFormData> } payload * @param { IEstimateFormData} payload
* @returns { void } * @returns { IEstimateType | undefined }
*/ */
updateEstimateSortOrder = async ( updateEstimateSwitch = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
payload: TEstimatePointsObject[] payload: IEstimateFormData
): Promise<void> => { ): Promise<IEstimateType | undefined> => {
try { try {
if (!this.id || !payload) return; if (!this.id || !payload) return;
const estimatePoints = await this.service.updateEstimate(workspaceSlug, projectId, this.id, { const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload);
estimate_points: payload, if (estimate) {
});
if (estimatePoints?.points && estimatePoints?.points.length > 0) {
runInAction(() => { runInAction(() => {
estimatePoints?.points.map((estimatePoint) => { this.name = estimate?.name;
this.type = estimate?.type;
estimate?.points &&
estimate?.points.map((estimatePoint) => {
if (estimatePoint.id) if (estimatePoint.id)
set(this.estimatePoints, [estimatePoint.id], new EstimatePoint(this.store, this.data, estimatePoint)); this.estimatePoints?.[estimatePoint.id]?.updateEstimatePointObject({
key: estimatePoint.key,
value: estimatePoint.value,
});
}); });
}); });
} }
return estimate;
} catch (error) { } catch (error) {
throw error; throw error;
} }