diff --git a/web/components/estimates/create/modal.tsx b/web/components/estimates/create/modal.tsx index 5ef3ee7de..76f94c6cf 100644 --- a/web/components/estimates/create/modal.tsx +++ b/web/components/estimates/create/modal.tsx @@ -132,7 +132,7 @@ export const CreateEstimateModal: FC = observer((props) => )} -
+
diff --git a/web/components/estimates/create/stage-one.tsx b/web/components/estimates/create/stage-one.tsx index 98e7c423e..7e3b5b009 100644 --- a/web/components/estimates/create/stage-one.tsx +++ b/web/components/estimates/create/stage-one.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; +import { Info } from "lucide-react"; import { TEstimateSystemKeys } from "@plane/types"; +import { RadioInput, Tooltip } from "@plane/ui"; // constants -import { RadioInput } from "@plane/ui"; import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; -// types type TEstimateCreateStageOne = { estimateSystem: TEstimateSystemKeys; @@ -18,41 +18,50 @@ export const EstimateCreateStageOne: FC = (props) => { if (!currentEstimateSystem) return <>; return ( -
+
{ const currentSystem = system as TEstimateSystemKeys; return { - label: ESTIMATE_SYSTEMS[currentSystem]?.name ||
Hello
, + label: !ESTIMATE_SYSTEMS[currentSystem]?.is_available ? ( +
+ {ESTIMATE_SYSTEMS[currentSystem]?.name} + + + +
+ ) : ( +
{ESTIMATE_SYSTEMS[currentSystem]?.name}
+ ), value: system, disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available, }; })} label="Choose an estimate system" - labelClassName="text-sm font-medium text-custom-text-200 mb-3" + labelClassName="text-sm font-medium text-custom-text-200 mb-2" wrapperClassName="relative flex flex-wrap gap-14" - fieldClassName="relative flex items-center gap-2" + fieldClassName="relative flex items-center gap-1.5" buttonClassName="size-4" selected={estimateSystem} onChange={(value) => handleEstimateSystem(value as TEstimateSystemKeys)} />
-
+
Start from scratch
-
+
Choose a template
{Object.keys(currentEstimateSystem.templates).map((name) => @@ -62,8 +71,8 @@ export const EstimateCreateStageOne: FC = (props) => { className="border border-custom-border-200 rounded-md p-3 py-2.5 text-left space-y-1 hover:bg-custom-background-90" onClick={() => handleEstimatePoints(name)} > -

{currentEstimateSystem.templates[name]?.title}

-

+

{currentEstimateSystem.templates[name]?.title}

+

{currentEstimateSystem.templates[name]?.values?.map((template) => template?.value)?.join(", ")}

diff --git a/web/components/estimates/estimate-disable-switch.tsx b/web/components/estimates/estimate-disable-switch.tsx index 5e0c7d88c..35b9b09cd 100644 --- a/web/components/estimates/estimate-disable-switch.tsx +++ b/web/components/estimates/estimate-disable-switch.tsx @@ -28,7 +28,7 @@ export const EstimateDisableSwitch: FC = observer((props setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Estimates have been disabled", + message: currentProjectActiveEstimate ? "Estimates have been disabled" : "Estimates have been enabled", }); } catch (err) { setToast({ diff --git a/web/components/estimates/points/create/preview.tsx b/web/components/estimates/points/create/preview.tsx index 313ea306e..1e7452812 100644 --- a/web/components/estimates/points/create/preview.tsx +++ b/web/components/estimates/points/create/preview.tsx @@ -34,7 +34,7 @@ export const EstimatePointItemCreatePreview: FC {estimatePoint?.value ? ( estimatePoint?.value ) : ( - Enter Estimate Value + Enter estimate point )}
= value={estimateInputValue} onChange={(e) => setEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" - placeholder="Enter estimate value" + placeholder="Enter estimate point" autoFocus /> {error && ( diff --git a/web/components/estimates/points/edit/create.tsx b/web/components/estimates/points/edit/create.tsx index 0220c4dc6..2c6561021 100644 --- a/web/components/estimates/points/edit/create.tsx +++ b/web/components/estimates/points/edit/create.tsx @@ -85,6 +85,7 @@ export const EstimatePointCreate: FC = observer((props) => value={estimateInputValue} onChange={(e) => setEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" + placeholder="Enter estimate point" autoFocus /> {error && ( diff --git a/web/components/estimates/points/edit/preview.tsx b/web/components/estimates/points/edit/preview.tsx index e7b6e1bf4..b3c509701 100644 --- a/web/components/estimates/points/edit/preview.tsx +++ b/web/components/estimates/points/edit/preview.tsx @@ -37,7 +37,11 @@ export const EstimatePointItemPreview: FC = observer(
- {estimatePoint?.value} + {estimatePoint?.value ? ( + estimatePoint?.value + ) : ( + Enter estimate point + )}
= observer((props) => value={estimateInputValue} onChange={(e) => setEstimateInputValue(e.target.value)} className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" + placeholder="Enter estimate point" autoFocus /> {error && ( diff --git a/web/components/estimates/points/switch/root.tsx b/web/components/estimates/points/switch/root.tsx index e3a17c543..34fb34010 100644 --- a/web/components/estimates/points/switch/root.tsx +++ b/web/components/estimates/points/switch/root.tsx @@ -113,7 +113,7 @@ export const EstimatePointSwitchRoot: FC = observer((p ))}
-
+
diff --git a/web/components/estimates/update/modal.tsx b/web/components/estimates/update/modal.tsx index 882175c44..902a579cd 100644 --- a/web/components/estimates/update/modal.tsx +++ b/web/components/estimates/update/modal.tsx @@ -88,7 +88,7 @@ export const UpdateEstimateModal: FC = observer((props) =>
{estimateEditType === undefined && ( -
+
diff --git a/web/helpers/estimates.ts b/web/helpers/estimates.ts new file mode 100644 index 000000000..45745da47 --- /dev/null +++ b/web/helpers/estimates.ts @@ -0,0 +1,33 @@ +import { EEstimateSystem } from "@plane/types/src/enums"; + +export const isEstimatePointValuesRepeated = ( + estimatePoints: string[], + estimateType: EEstimateSystem, + newEstimatePoint?: string | undefined +) => { + const currentEstimatePoints = estimatePoints.map((estimatePoint) => estimatePoint.trim()); + let isValid = false; + + if (newEstimatePoint === undefined) { + if (estimateType === EEstimateSystem.CATEGORIES) { + const points = new Set(currentEstimatePoints); + if (points.size === currentEstimatePoints.length) isValid = true; + } else if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType)) { + currentEstimatePoints.map((point) => { + if (Number(point) === Number(newEstimatePoint)) isValid = true; + }); + } + } else { + if (estimateType === EEstimateSystem.CATEGORIES) { + currentEstimatePoints.map((point) => { + if (point === newEstimatePoint.trim()) isValid = true; + }); + } else if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateType)) { + currentEstimatePoints.map((point) => { + if (Number(point) === Number(newEstimatePoint.trim())) isValid = true; + }); + } + } + + return isValid; +}; diff --git a/web/services/project/estimate.service.ts b/web/services/project/estimate.service.ts index e1af542ca..cb8512eb8 100644 --- a/web/services/project/estimate.service.ts +++ b/web/services/project/estimate.service.ts @@ -5,7 +5,7 @@ import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; -export class EstimateService extends APIService { +class EstimateService extends APIService { constructor() { super(API_BASE_URL); } @@ -125,3 +125,6 @@ export class EstimateService extends APIService { }); } } +const estimateService = new EstimateService(); + +export default estimateService; diff --git a/web/services/project/index.ts b/web/services/project/index.ts index 10ea7eb9b..73121627c 100644 --- a/web/services/project/index.ts +++ b/web/services/project/index.ts @@ -1,5 +1,4 @@ export * from "./project.service"; -export * from "./project-estimate.service"; export * from "./estimate.service"; export * from "./project-export.service"; export * from "./project-member.service"; diff --git a/web/services/project/project-estimate.service.ts b/web/services/project/project-estimate.service.ts deleted file mode 100644 index f15ae4837..000000000 --- a/web/services/project/project-estimate.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -// services -import { API_BASE_URL } from "@/helpers/common.helper"; -import { APIService } from "@/services/api.service"; -// types -import type { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types"; -// helpers - -export class ProjectEstimateService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async createEstimate( - workspaceSlug: string, - projectId: string, - data: IEstimateFormData - ): Promise<{ - estimate: IEstimate; - estimate_points: IEstimatePoint[]; - }> { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } - - async patchEstimate( - workspaceSlug: string, - projectId: string, - estimateId: string, - data: IEstimateFormData - ): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getEstimateDetails(workspaceSlug: string, projectId: string, estimateId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getEstimatesList(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getWorkspaceEstimatesList(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/estimates/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } -} diff --git a/web/store/estimates/estimate-point.ts b/web/store/estimates/estimate-point.ts index f2296c7bc..3b4bf48b7 100644 --- a/web/store/estimates/estimate-point.ts +++ b/web/store/estimates/estimate-point.ts @@ -2,7 +2,7 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { IEstimate, IEstimatePoint as IEstimatePointType } from "@plane/types"; // services -import { EstimateService } from "@/services/project/estimate.service"; +import estimateService from "@/services/project/estimate.service"; // store import { RootStore } from "@/store/root.store"; @@ -41,8 +41,6 @@ export class EstimatePoint implements IEstimatePoint { updated_by: string | undefined = undefined; // observables error: TErrorCodes | undefined = undefined; - // service - service: EstimateService; constructor( private store: RootStore, @@ -80,8 +78,6 @@ export class EstimatePoint implements IEstimatePoint { this.updated_at = this.data.updated_at; this.created_by = this.data.created_by; this.updated_by = this.data.updated_by; - // service - this.service = new EstimateService(); } // computed @@ -102,6 +98,11 @@ export class EstimatePoint implements IEstimatePoint { } // helper actions + /** + * @description updating an estimate point object in local store + * @param { Partial } estimatePoint + * @returns { void } + */ updateEstimatePointObject = (estimatePoint: Partial) => { Object.keys(estimatePoint).map((key) => { const estimatePointKey = key as keyof IEstimatePointType; @@ -123,7 +124,7 @@ export class EstimatePoint implements IEstimatePoint { try { if (!this.projectEstimate?.id || !this.id || !payload) return undefined; - const estimatePoint = await this.service.updateEstimatePoint( + const estimatePoint = await estimateService.updateEstimatePoint( workspaceSlug, projectId, this.projectEstimate?.id, diff --git a/web/store/estimates/estimate.ts b/web/store/estimates/estimate.ts index 9db038edd..8417386c9 100644 --- a/web/store/estimates/estimate.ts +++ b/web/store/estimates/estimate.ts @@ -11,7 +11,7 @@ import { TEstimatePointsObject, } from "@plane/types"; // services -import { EstimateService } from "@/services/project/estimate.service"; +import estimateService from "@/services/project/estimate.service"; // store import { IEstimatePoint, EstimatePoint } from "@/store/estimates/estimate-point"; import { RootStore } from "@/store/root.store"; @@ -69,8 +69,6 @@ export class Estimate implements IEstimate { // observables error: TErrorCodes | undefined = undefined; estimatePoints: Record = {}; - // service - service: EstimateService; constructor( private store: RootStore, @@ -116,8 +114,6 @@ export class Estimate implements IEstimate { if (estimationPoint.id) set(this.estimatePoints, [estimationPoint.id], new EstimatePoint(this.store, this.data, estimationPoint)); }); - // service - this.service = new EstimateService(); } // computed @@ -169,7 +165,7 @@ export class Estimate implements IEstimate { try { if (!this.id || !payload) return; - const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, { + const estimate = await estimateService.updateEstimate(workspaceSlug, projectId, this.id, { estimate_points: payload, }); runInAction(() => { @@ -201,7 +197,7 @@ export class Estimate implements IEstimate { try { if (!this.id || !payload) return; - const estimate = await this.service.updateEstimate(workspaceSlug, projectId, this.id, payload); + const estimate = await estimateService.updateEstimate(workspaceSlug, projectId, this.id, payload); if (estimate) { runInAction(() => { this.name = estimate?.name; @@ -238,7 +234,7 @@ export class Estimate implements IEstimate { try { if (!this.id || !payload) return; - const estimatePoint = await this.service.createEstimatePoint(workspaceSlug, projectId, this.id, payload); + const estimatePoint = await estimateService.createEstimatePoint(workspaceSlug, projectId, this.id, payload); if (estimatePoint) { runInAction(() => { if (estimatePoint.id) { @@ -268,7 +264,7 @@ export class Estimate implements IEstimate { try { if (!this.id) return; - const deleteEstimatePoint = await this.service.removeEstimatePoint( + const deleteEstimatePoint = await estimateService.removeEstimatePoint( workspaceSlug, projectId, this.id, diff --git a/web/store/estimates/project-estimate.store.ts b/web/store/estimates/project-estimate.store.ts index 49a37451d..dbedbf131 100644 --- a/web/store/estimates/project-estimate.store.ts +++ b/web/store/estimates/project-estimate.store.ts @@ -5,7 +5,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" import { computedFn } from "mobx-utils"; import { IEstimate as IEstimateType, IEstimateFormData } from "@plane/types"; // services -import { EstimateService } from "@/services/project/estimate.service"; +import estimateService from "@/services/project/estimate.service"; // store import { IEstimate, Estimate } from "@/store/estimates/estimate"; import { RootStore } from "@/store/root.store"; @@ -47,8 +47,6 @@ export class ProjectEstimateStore implements IProjectEstimateStore { loader: TEstimateLoader = undefined; estimates: Record = {}; // estimate_id -> estimate error: TErrorCodes | undefined = undefined; - // service - service: EstimateService; constructor(private store: RootStore) { makeObservable(this, { @@ -65,12 +63,9 @@ export class ProjectEstimateStore implements IProjectEstimateStore { getEstimateById: action, createEstimate: action, }); - // service - this.service = new EstimateService(); } // computed - /** * @description get current active estimate id for a project * @returns { string | undefined } @@ -146,7 +141,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { this.error = undefined; if (Object.keys(this.estimates || {}).length <= 0) this.loader = loader ? loader : "init-loader"; - const estimates = await this.service.fetchWorkspaceEstimates(workspaceSlug); + const estimates = await estimateService.fetchWorkspaceEstimates(workspaceSlug); if (estimates && estimates.length > 0) { runInAction(() => { estimates.forEach((estimate) => { @@ -181,7 +176,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { this.error = undefined; if (!this.estimateIdsByProjectId(projectId)) this.loader = loader ? loader : "init-loader"; - const estimates = await this.service.fetchProjectEstimates(workspaceSlug, projectId); + const estimates = await estimateService.fetchProjectEstimates(workspaceSlug, projectId); if (estimates && estimates.length > 0) { runInAction(() => { estimates.forEach((estimate) => { @@ -216,7 +211,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { try { this.error = undefined; - const estimate = await this.service.fetchEstimateById(workspaceSlug, projectId, estimateId); + const estimate = await estimateService.fetchEstimateById(workspaceSlug, projectId, estimateId); if (estimate) { runInAction(() => { if (estimate.id) @@ -252,7 +247,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { try { this.error = undefined; - const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload); + const estimate = await estimateService.createEstimate(workspaceSlug, projectId, payload); if (estimate) { await this.store.projectRoot.project.updateProject(workspaceSlug, projectId, { estimate: estimate.id,