mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: updated switch estimate
This commit is contained in:
parent
39482b72ab
commit
9c279a62e0
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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}/`,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user