From 73e36cef380981800886fedaf0c98db901a2f784 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 18 Dec 2023 16:26:17 +0530 Subject: [PATCH] chore: initialize and implement project estimate store --- web/components/core/activity.tsx | 21 +- .../create-update-estimate-modal.tsx | 24 +- .../estimates/delete-estimate-modal.tsx | 25 +- web/components/estimates/estimates-list.tsx | 36 ++- web/components/issues/select/estimate.tsx | 25 +- .../issues/sidebar-select/estimate.tsx | 24 +- web/components/issues/sidebar.tsx | 47 ++-- .../issues/view-select/estimate.tsx | 27 +-- web/hooks/store/index.ts | 2 + web/hooks/store/use-estimate.ts | 11 + web/hooks/use-estimate-option.tsx | 45 ---- web/layouts/auth-layout/project-wrapper.tsx | 3 +- .../project/project-estimate.service.ts | 11 +- web/store/estimate.store.ts | 228 ++++++++++++++++++ web/store/root.store.ts | 7 +- web/types/estimate.d.ts | 14 +- 16 files changed, 370 insertions(+), 180 deletions(-) create mode 100644 web/hooks/store/use-estimate.ts delete mode 100644 web/hooks/use-estimate-option.tsx create mode 100644 web/store/estimate.store.ts diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 85b4a8bb9..e0e00dd73 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,10 +1,8 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -// hooks -import { useLabel } from "hooks/store"; -// hook -import useEstimateOption from "hooks/use-estimate-option"; +// store hooks +import { useEstimate, useLabel } from "hooks/store"; // icons import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { @@ -76,7 +74,7 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => { const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { // store hooks const { - workspaceLabel: { workspaceLabels, fetchWorkspaceLabels }, + workspace: { workspaceLabels, fetchWorkspaceLabels }, } = useLabel(); useEffect(() => { @@ -94,16 +92,21 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works ); }); -const EstimatePoint = ({ point }: { point: string }) => { - const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); +const EstimatePoint = observer((props: { point: string }) => { + const { point } = props; + const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate(); const currentPoint = Number(point) + 1; + const estimateValue = getEstimatePointValue(Number(point)); + return ( - {isEstimateActive ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} + {areEstimatesEnabledForCurrentProject + ? estimateValue + : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} ); -}; +}); const activityDetails: { [key: string]: { diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 6f0f98e86..4a6bb205f 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -2,11 +2,9 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; - -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { Button, Input, TextArea } from "@plane/ui"; @@ -36,16 +34,14 @@ type FormValues = typeof defaultValues; export const CreateUpdateEstimateModal: React.FC = observer((props) => { const { handleClose, data, isOpen } = props; - // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { - projectEstimates: { createEstimate, updateEstimate }, - } = useMobxStore(); - + // store hooks + const { createEstimate, updateEstimate } = useEstimate(); + // form info + // toast alert + const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -60,8 +56,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { reset(); }; - const { setToastAlert } = useToast(); - const handleCreateEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId) return; @@ -299,8 +293,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { ? "Updating Estimate..." : "Update Estimate" : isSubmitting - ? "Creating Estimate..." - : "Create Estimate"} + ? "Creating Estimate..." + : "Create Estimate"} diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index c9d34fe8e..a4041e00a 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +import { AlertTriangle } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // types import { IEstimate } from "types"; -// icons -import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; @@ -21,18 +19,14 @@ type Props = { export const DeleteEstimateModal: React.FC = observer((props) => { const { isOpen, handleClose, data } = props; - + // states + const [isDeleteLoading, setIsDeleteLoading] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { projectEstimates: projectEstimatesStore } = useMobxStore(); - - // states - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - // hooks + // store hooks + const { deleteEstimate } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); const handleEstimateDelete = () => { @@ -40,8 +34,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const estimateId = data?.id!; - projectEstimatesStore - .deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) + deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) .then(() => { setIsDeleteLoading(false); handleClose(); diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 29debfedb..95e9f83a5 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,40 +1,36 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -// store import { observer } from "mobx-react-lite"; +import { Plus } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; +import useToast from "hooks/use-toast"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -//hooks -import useToast from "hooks/use-toast"; // ui import { Button, Loader } from "@plane/ui"; import { EmptyState } from "components/common"; -// icons -import { Plus } from "lucide-react"; // images import emptyEstimate from "public/empty-state/estimate.svg"; // types import { IEstimate } from "types"; export const EstimatesList: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - // store - const { - project: { currentProjectDetails, updateProject }, - projectEstimates: { projectEstimates, getProjectEstimateById }, - } = useMobxStore(); // states const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateToDelete, setEstimateToDelete] = useState(null); const [estimateToUpdate, setEstimateToUpdate] = useState(); - // hooks + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { + project: { currentProjectDetails, updateProject }, + } = useMobxStore(); + const { projectEstimates, getProjectEstimateById } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); - // derived values - const estimatesList = projectEstimates; const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -96,10 +92,10 @@ export const EstimatesList: React.FC = observer(() => { - {estimatesList ? ( - estimatesList.length > 0 ? ( + {projectEstimates ? ( + projectEstimates.length > 0 ? (
- {estimatesList.map((estimate) => ( + {projectEstimates.map((estimate) => ( void; }; -export const IssueEstimateSelect: React.FC = ({ value, onChange }) => { - const { isEstimateActive, estimatePoints } = useEstimateOption(); +export const IssueEstimateSelect: React.FC = observer((props) => { + const { value, onChange } = props; - if (!isEstimateActive) return null; + const { areEstimatesEnabledForCurrentProject, activeEstimateDetails } = useEstimate(); + + if (!areEstimatesEnabledForCurrentProject) return null; return ( = ({ value, onChange }) => {
- {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"} + {activeEstimateDetails?.points?.find((e) => e.key === value)?.value ?? "Estimate"}
} @@ -40,8 +41,8 @@ export const IssueEstimateSelect: React.FC = ({ value, onChange }) => { None - {estimatePoints && - estimatePoints.map((point) => ( + {activeEstimateDetails?.points && + activeEstimateDetails.points?.map((point) => ( <> @@ -53,4 +54,4 @@ export const IssueEstimateSelect: React.FC = ({ value, onChange }) => { ))}
); -}; +}); diff --git a/web/components/issues/sidebar-select/estimate.tsx b/web/components/issues/sidebar-select/estimate.tsx index 3005946fe..e865b2aaa 100644 --- a/web/components/issues/sidebar-select/estimate.tsx +++ b/web/components/issues/sidebar-select/estimate.tsx @@ -1,11 +1,10 @@ import React from "react"; - -// hooks -import useEstimateOption from "hooks/use-estimate-option"; +import { observer } from "mobx-react-lite"; +import { Triangle } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; // ui import { CustomSelect } from "@plane/ui"; -// icons -import { Triangle } from "lucide-react"; type Props = { value: number | null; @@ -13,10 +12,13 @@ type Props = { disabled?: boolean; }; -export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabled = false }) => { - const { estimatePoints } = useEstimateOption(); +export const SidebarEstimateSelect: React.FC = observer((props) => { + const { value, onChange, disabled = false } = props; + + const { activeEstimateDetails, getEstimatePointValue } = useEstimate(); + + const currentEstimate = getEstimatePointValue(value); - const currentEstimate = estimatePoints?.find((e) => e.key === value)?.value; return ( = ({ value, onChange, disabl None - {estimatePoints && - estimatePoints.map((point) => ( + {activeEstimateDetails?.points && + activeEstimateDetails?.points?.map((point) => ( <> @@ -56,4 +58,4 @@ export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabl ))} ); -}; +}); diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index 90278d96b..f870f0ec9 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -5,11 +5,10 @@ import { mutate } from "swr"; import { Controller, UseFormWatch } from "react-hook-form"; import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react"; // hooks -import { useProjectState, useUser } from "hooks/store"; +import { useEstimate, useProjectState, useUser } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast"; import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription"; -import useEstimateOption from "hooks/use-estimate-option"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; @@ -89,12 +88,11 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { membership: { currentProjectRole }, } = useUser(); const { projectStates } = useProjectState(); + const { areEstimatesEnabledForCurrentProject } = useEstimate(); // router const router = useRouter(); const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query; - const { isEstimateActive } = useEstimateOption(); - const { loading, handleSubscribe, handleUnsubscribe, subscribed } = useUserIssueNotificationSubscription( workspaceSlug, projectId, @@ -341,27 +339,28 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && isEstimateActive && ( -
-
- -

Estimate

+ {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesEnabledForCurrentProject && ( +
+
+ +

Estimate

+
+
+ ( + submitChanges({ estimate_point: val })} + disabled={!isAllowed || uneditable} + /> + )} + /> +
-
- ( - submitChanges({ estimate_point: val })} - disabled={!isAllowed || uneditable} - /> - )} - /> -
-
- )} + )}
)} {showSecondSection && ( diff --git a/web/components/issues/view-select/estimate.tsx b/web/components/issues/view-select/estimate.tsx index b161eb598..4676af44e 100644 --- a/web/components/issues/view-select/estimate.tsx +++ b/web/components/issues/view-select/estimate.tsx @@ -1,10 +1,10 @@ import React from "react"; -// hooks -import useEstimateOption from "hooks/use-estimate-option"; +import { observer } from "mobx-react-lite"; +import { Triangle } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; // ui import { CustomSelect, Tooltip } from "@plane/ui"; -// icons -import { Triangle } from "lucide-react"; // types import { IIssue } from "types"; @@ -16,16 +16,11 @@ type Props = { disabled: boolean; }; -export const ViewEstimateSelect: React.FC = ({ - issue, - onChange, - tooltipPosition = "top", - customButton = false, - disabled, -}) => { - const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point); +export const ViewEstimateSelect: React.FC = observer((props) => { + const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props; + const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate(); - const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value; + const estimateValue = getEstimatePointValue(issue.estimate_point); const estimateLabels = ( @@ -36,7 +31,7 @@ export const ViewEstimateSelect: React.FC = ({ ); - if (!isEstimateActive) return null; + if (!areEstimatesEnabledForCurrentProject) return null; return ( = ({ None - {estimatePoints?.map((estimate) => ( + {activeEstimateDetails?.points?.map((estimate) => ( <> @@ -66,4 +61,4 @@ export const ViewEstimateSelect: React.FC = ({ ))} ); -}; +}); diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 4b497e388..a80e61b4d 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,5 +1,7 @@ export * from "./use-application"; export * from "./use-cycle"; +export * from "./use-estimate"; +export * from "./use-inbox"; export * from "./use-label"; export * from "./use-member"; export * from "./use-module"; diff --git a/web/hooks/store/use-estimate.ts b/web/hooks/store/use-estimate.ts new file mode 100644 index 000000000..cbb38d778 --- /dev/null +++ b/web/hooks/store/use-estimate.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "contexts/store-context"; +// types +import { IProjectEstimateStore } from "store/estimate.store"; + +export const useEstimate = (): IProjectEstimateStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useCycle must be used within StoreProvider"); + return context.estimate; +}; diff --git a/web/hooks/use-estimate-option.tsx b/web/hooks/use-estimate-option.tsx deleted file mode 100644 index 6d64c9665..000000000 --- a/web/hooks/use-estimate-option.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useRouter } from "next/router"; -import useSWR from "swr"; -// services -import { ProjectEstimateService } from "services/project"; -// hooks -import useProjectDetails from "hooks/use-project-details"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; -// fetch-keys -import { ESTIMATE_DETAILS } from "constants/fetch-keys"; - -// services -const projectEstimateService = new ProjectEstimateService(); - -const useEstimateOption = (estimateKey?: number | null) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { projectDetails } = useProjectDetails(); - - const { data: estimateDetails } = useSWR( - workspaceSlug && projectId && projectDetails && projectDetails?.estimate - ? ESTIMATE_DETAILS(projectDetails.estimate as string) - : null, - workspaceSlug && projectId && projectDetails && projectDetails.estimate - ? () => - projectEstimateService.getEstimateDetails( - workspaceSlug.toString(), - projectId.toString(), - projectDetails.estimate as string - ) - : null - ); - - const estimateValue: any = - estimateKey || estimateKey === 0 ? estimateDetails?.points?.find((e) => e.key === estimateKey)?.value : "None"; - - return { - isEstimateActive: projectDetails?.estimate ? true : false, - estimatePoints: orderArrayBy(estimateDetails?.points ?? [], "key"), - estimateValue, - }; -}; - -export default useEstimateOption; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index 04e8aa9ac..32a977407 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -6,6 +6,7 @@ import useSWR from "swr"; import { useApplication, useCycle, + useEstimate, useLabel, useMember, useModule, @@ -30,7 +31,6 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store const { - projectEstimates: { fetchProjectEstimates }, inbox: { fetchInboxesList, isInboxEnabled }, } = useMobxStore(); const { @@ -50,6 +50,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { project: { fetchProjectLabels }, } = useLabel(); + const { fetchProjectEstimates } = useEstimate(); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/web/services/project/project-estimate.service.ts b/web/services/project/project-estimate.service.ts index 4a849868c..efa2c8444 100644 --- a/web/services/project/project-estimate.service.ts +++ b/web/services/project/project-estimate.service.ts @@ -1,7 +1,7 @@ // services import { APIService } from "services/api.service"; // types -import type { IEstimate, IEstimateFormData } from "types"; +import type { IEstimate, IEstimateFormData, IEstimatePoint } from "types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; @@ -10,7 +10,14 @@ export class ProjectEstimateService extends APIService { super(API_BASE_URL); } - async createEstimate(workspaceSlug: string, projectId: string, data: IEstimateFormData): Promise { + 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) => { diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts new file mode 100644 index 000000000..ca2590ab8 --- /dev/null +++ b/web/store/estimate.store.ts @@ -0,0 +1,228 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +import { set } from "lodash"; +// services +import { ProjectEstimateService } from "services/project"; +// types +import { RootStore } from "store/root.store"; +import { IEstimate, IEstimateFormData } from "types"; + +export interface IProjectEstimateStore { + // states + loader: boolean; + error: any | null; + // observables + estimates: Record; + // computed + areEstimatesEnabledForCurrentProject: boolean; + projectEstimates: IEstimate[] | null; + activeEstimateDetails: IEstimate | null; + // computed actions + getEstimatePointValue: (estimateKey: number | null) => string; + getProjectEstimateById: (estimateId: string) => IEstimate | null; + // actions + fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; + createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise; + updateEstimate: ( + workspaceSlug: string, + projectId: string, + estimateId: string, + data: IEstimateFormData + ) => Promise; + deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise; +} + +export class ProjectEstimatesStore implements IProjectEstimateStore { + // states + loader: boolean = false; + error: any | null = null; + // observables + estimates: Record = {}; + // root store + rootStore; + // services + estimateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable, + error: observable, + // observables + estimates: observable, + // computed + areEstimatesEnabledForCurrentProject: computed, + projectEstimates: computed, + activeEstimateDetails: computed, + // computed actions + getProjectEstimateById: action, + getEstimatePointValue: action, + // actions + fetchProjectEstimates: action, + createEstimate: action, + updateEstimate: action, + deleteEstimate: action, + }); + + // root store + this.rootStore = _rootStore; + // services + this.estimateService = new ProjectEstimateService(); + } + + /** + * @description returns true if estimates are enabled for current project, false otherwise + */ + get areEstimatesEnabledForCurrentProject() { + const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; + + if (!currentProjectDetails) return false; + + return Boolean(currentProjectDetails?.estimate); + } + + /** + * @description returns the list of estimates for current project + */ + get projectEstimates() { + const projectId = this.rootStore.app.router.projectId; + + if (!projectId) return null; + return this.estimates?.[projectId] || null; + } + + /** + * @description returns the active estimate details for current project + */ + get activeEstimateDetails() { + const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; + + if (!currentProjectDetails || !currentProjectDetails?.estimate) return null; + + return this.projectEstimates?.find((estimate) => estimate.id === currentProjectDetails?.estimate) || null; + } + + /** + * @description returns the point value for the given estimate key to display in the UI + */ + getEstimatePointValue = (estimateKey: number | null) => { + if (estimateKey === null) return "None"; + + const activeEstimate = this.activeEstimateDetails; + + return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None"; + }; + + /** + * @description returns the estimate details for the given estimate id + */ + getProjectEstimateById = (estimateId: string) => { + if (!this.projectEstimates) return null; + + const estimateInfo = this.projectEstimates?.find((estimate) => estimate.id === estimateId) || null; + return estimateInfo; + }; + + /** + * @description fetches the list of estimates for the given project + * @param workspaceSlug + * @param projectId + */ + fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectId); + + runInAction(() => { + set(this.estimates, projectId, estimatesResponse); + this.loader = false; + this.error = null; + }); + + return estimatesResponse; + } catch (error) { + this.loader = false; + this.error = error; + + throw error; + } + }; + + /** + * @description creates a new estimate for the given project + * @param workspaceSlug + * @param projectId + * @param data + */ + createEstimate = async (workspaceSlug: string, projectId: string, data: IEstimateFormData) => { + try { + const response = await this.estimateService.createEstimate(workspaceSlug, projectId, data); + + const responseEstimate = { + ...response.estimate, + points: response.estimate_points, + }; + + runInAction(() => { + set(this.estimates, projectId, [responseEstimate, ...(this.estimates?.[projectId] || [])]); + }); + + return response.estimate; + } catch (error) { + console.log("Failed to create estimate from project store"); + throw error; + } + }; + + /** + * @description updates the given estimate for the given project + * @param workspaceSlug + * @param projectId + * @param estimateId + * @param data + */ + updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) => { + try { + const updatedEstimates = (this.estimates?.[projectId] ?? []).map((estimate) => + estimate.id === estimateId ? { ...estimate, ...data.estimate } : estimate + ); + + runInAction(() => { + set(this.estimates, projectId, updatedEstimates); + }); + + const response = await this.estimateService.patchEstimate(workspaceSlug, projectId, estimateId, data); + + return response; + } catch (error) { + console.log("Failed to update estimate from project store"); + + this.fetchProjectEstimates(workspaceSlug, projectId); + + throw error; + } + }; + + /** + * @description deletes the given estimate for the given project + * @param workspaceSlug + * @param projectId + * @param estimateId + */ + deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => { + try { + const updatedEstimates = (this.estimates?.[projectId] ?? []).filter((estimate) => estimate.id !== estimateId); + + runInAction(() => { + set(this.estimates, projectId, updatedEstimates); + }); + + await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId); + } catch (error) { + console.log("Failed to delete estimate from project store"); + + this.fetchProjectEstimates(workspaceSlug, projectId); + } + }; +} diff --git a/web/store/root.store.ts b/web/store/root.store.ts index e97dd5523..219ae76d7 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -13,6 +13,7 @@ import { IPageStore, PageStore } from "./page.store"; import { ILabelRootStore, LabelRootStore } from "./label"; import { IMemberRootStore, MemberRootStore } from "./member"; import { IInboxRootStore, InboxRootStore } from "./inbox"; +import { IProjectEstimateStore, ProjectEstimatesStore } from "./estimate.store"; enableStaticRendering(typeof window === "undefined"); @@ -30,6 +31,7 @@ export class RootStore { page: IPageStore; issue: IIssueRootStore; state: IStateStore; + estimate: IProjectEstimateStore; constructor() { this.app = new AppRootStore(this); @@ -40,11 +42,12 @@ export class RootStore { this.memberRoot = new MemberRootStore(this); this.inboxRoot = new InboxRootStore(this); // independent stores - this.state = new StateStore(this); - this.issue = new IssueRootStore(this); this.cycle = new CycleStore(this); this.module = new ModulesStore(this); this.projectView = new ProjectViewStore(this); this.page = new PageStore(this); + this.issue = new IssueRootStore(this); + this.state = new StateStore(this); + this.estimate = new ProjectEstimatesStore(this); } } diff --git a/web/types/estimate.d.ts b/web/types/estimate.d.ts index 32925c793..96b584ce1 100644 --- a/web/types/estimate.d.ts +++ b/web/types/estimate.d.ts @@ -1,24 +1,24 @@ export interface IEstimate { - id: string; created_at: Date; - updated_at: Date; - name: string; - description: string; created_by: string; - updated_by: string; - points: IEstimatePoint[]; + description: string; + id: string; + name: string; project: string; project_detail: IProject; + updated_at: Date; + updated_by: string; + points: IEstimatePoint[]; workspace: string; workspace_detail: IWorkspace; } export interface IEstimatePoint { - id: string; created_at: string; created_by: string; description: string; estimate: string; + id: string; key: number; project: string; updated_at: string;