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):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Estimate

View File

@ -122,7 +122,12 @@ class BulkEstimatePointEndpoint(BaseViewSet):
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", [])
@ -159,13 +164,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
batch_size=10,
)
estimate_point_serializer = EstimatePointSerializer(
estimate_points, many=True
)
estimate_serializer = EstimateReadSerializer(estimate)
return Response(
{
"points": estimate_point_serializer.data,
},
estimate_serializer.data,
status=status.HTTP_200_OK,
)

View File

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

View File

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

View File

@ -1,25 +1,28 @@
import { FC, useEffect, useState } from "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
import { EstimatePointItemSwitchPreview } from "@/components/estimates/points";
// constants
import { EEstimateSystem, EEstimateUpdateStages } from "@/constants/estimates";
import { EEstimateSystem, EEstimateUpdateStages, ESTIMATE_SYSTEMS } from "@/constants/estimates";
// hooks
import { useEstimate } from "@/hooks/store";
type TEstimatePointSwitchRoot = {
estimateSystemSwitchType: TEstimateSystemKeys;
workspaceSlug: string;
projectId: string;
estimateId: string;
handleClose: () => void;
mode?: EEstimateUpdateStages;
};
export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((props) => {
// props
const { workspaceSlug, projectId, estimateId } = props;
const { estimateSystemSwitchType, workspaceSlug, projectId, estimateId, handleClose } = props;
// hooks
const { asJson: estimate, estimatePointIds, estimatePointById } = useEstimate(estimateId);
const { asJson: estimate, estimatePointIds, estimatePointById, updateEstimateSwitch } = useEstimate(estimateId);
// states
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 <></>;
return (
<>
<div className="space-y-3">
<div className="text-sm font-medium flex items-center gap-2">
<div className="w-full">Current {estimate?.type}</div>
<div className="flex-shrink-0 w-4 h-4" />
<div className="w-full">
New {estimate?.type === EEstimateSystem?.POINTS ? EEstimateSystem?.CATEGORIES : EEstimateSystem?.POINTS}
</div>
<div className="w-full">New {estimateSystemSwitchType}</div>
</div>
{estimatePoints.map((estimateObject, index) => (
@ -62,5 +112,16 @@ export const EstimatePointSwitchRoot: FC<TEstimatePointSwitchRoot> = observer((p
/>
))}
</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 { observer } from "mobx-react";
import { ChevronLeft } from "lucide-react";
import { TEstimateUpdateStageKeys } from "@plane/types";
import { TEstimateSystemKeys, TEstimateUpdateStageKeys } from "@plane/types";
import { Button } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { EstimateUpdateStageOne, EstimatePointEditRoot, EstimatePointSwitchRoot } from "@/components/estimates";
// constants
import { EEstimateUpdateStages } from "@/constants/estimates";
import { EEstimateSystem, EEstimateUpdateStages } from "@/constants/estimates";
// hooks
import { useEstimate } from "@/hooks/store";
type TUpdateEstimateModal = {
workspaceSlug: string;
@ -20,16 +22,26 @@ type TUpdateEstimateModal = {
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) => {
// props
const { workspaceSlug, projectId, estimateId, isOpen, handleClose } = props;
// hooks
const { asJson: estimate } = useEstimate(estimateId);
// states
const [estimateEditType, setEstimateEditType] = useState<TEstimateUpdateStageKeys | undefined>(undefined);
const [estimateSystemSwitchType, setEstimateSystemSwitchType] = useState<TEstimateSystemKeys | undefined>(undefined);
useEffect(() => {
if (!isOpen) setEstimateEditType(undefined);
if (!isOpen) {
setEstimateEditType(undefined);
setEstimateSystemSwitchType(undefined);
}
}, [isOpen]);
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => setEstimateEditType(type);
const handleSwitchEstimate = () => {};
const handleEstimateEditType = (type: TEstimateUpdateStageKeys) => {
if (type === EEstimateUpdateStages.SWITCH && estimate?.type)
setEstimateSystemSwitchType(
estimate?.type === EEstimateSystem.CATEGORIES ? EEstimateSystem.POINTS : EEstimateSystem.CATEGORIES
);
setEstimateEditType(type);
};
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
@ -62,23 +74,24 @@ export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer((props) =>
{estimateEditType === EEstimateUpdateStages.EDIT && (
<EstimatePointEditRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} />
)}
{estimateEditType === EEstimateUpdateStages.SWITCH && (
<EstimatePointSwitchRoot workspaceSlug={workspaceSlug} projectId={projectId} estimateId={estimateId} />
{estimateEditType === EEstimateUpdateStages.SWITCH && estimateSystemSwitchType && (
<EstimatePointSwitchRoot
estimateSystemSwitchType={estimateSystemSwitchType}
workspaceSlug={workspaceSlug}
projectId={projectId}
estimateId={estimateId}
handleClose={handleClose}
/>
)}
</>
)}
</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">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
{estimateEditType === EEstimateUpdateStages.SWITCH && (
<Button variant="primary" size="sm" onClick={handleSwitchEstimate}>
Update
</Button>
)}
</div>
)}
</div>

View File

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

View File

@ -16,6 +16,8 @@ export interface IEstimatePoint extends IEstimatePointType {
error: TErrorCodes | undefined;
// computed
asJson: IEstimatePointType;
// helper actions
updateEstimatePointObject: (estimatePoint: Partial<IEstimatePointType>) => void;
// actions
updateEstimatePoint: (
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
/**
* @description updating an estimate point

View File

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