mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: initialize and implement project estimate store
This commit is contained in:
parent
34e4b6e2ff
commit
73e36cef38
@ -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 (
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{isEstimateActive ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
{areEstimatesEnabledForCurrentProject
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
|
@ -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<Props> = 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<Props> = observer((props) => {
|
||||
reset();
|
||||
};
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCreateEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@ -299,8 +293,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = observer((props) => {
|
||||
? "Updating Estimate..."
|
||||
: "Update Estimate"
|
||||
: isSubmitting
|
||||
? "Creating Estimate..."
|
||||
: "Create Estimate"}
|
||||
? "Creating Estimate..."
|
||||
: "Create Estimate"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -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<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
const estimateId = data?.id!;
|
||||
|
||||
projectEstimatesStore
|
||||
.deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
||||
deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
||||
.then(() => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
|
@ -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<string | null>(null);
|
||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
||||
// 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(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{estimatesList ? (
|
||||
estimatesList.length > 0 ? (
|
||||
{projectEstimates ? (
|
||||
projectEstimates.length > 0 ? (
|
||||
<section className="h-full overflow-y-auto bg-custom-background-100">
|
||||
{estimatesList.map((estimate) => (
|
||||
{projectEstimates.map((estimate) => (
|
||||
<EstimateListItem
|
||||
key={estimate.id}
|
||||
estimate={estimate}
|
||||
|
@ -1,21 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
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";
|
||||
// fetch-keys
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
type Props = {
|
||||
value: number | null;
|
||||
onChange: (value: number | null) => void;
|
||||
};
|
||||
|
||||
export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||
export const IssueEstimateSelect: React.FC<Props> = observer((props) => {
|
||||
const { value, onChange } = props;
|
||||
|
||||
if (!isEstimateActive) return null;
|
||||
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails } = useEstimate();
|
||||
|
||||
if (!areEstimatesEnabledForCurrentProject) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
@ -24,7 +25,7 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
<div className="flex items-center justify-center gap-1 text-xs">
|
||||
<Triangle className={`h-3 w-3 ${value !== null ? "text-custom-text-200" : "text-custom-text-300"}`} />
|
||||
<span className={value !== null ? "text-custom-text-200" : "text-custom-text-300"}>
|
||||
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
|
||||
{activeEstimateDetails?.points?.find((e) => e.key === value)?.value ?? "Estimate"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@ -40,8 +41,8 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{estimatePoints &&
|
||||
estimatePoints.map((point) => (
|
||||
{activeEstimateDetails?.points &&
|
||||
activeEstimateDetails.points?.map((point) => (
|
||||
<CustomSelect.Option key={point.key} value={point.key}>
|
||||
<>
|
||||
<span>
|
||||
@ -53,4 +54,4 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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<Props> = ({ value, onChange, disabled = false }) => {
|
||||
const { estimatePoints } = useEstimateOption();
|
||||
export const SidebarEstimateSelect: React.FC<Props> = 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 (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
@ -43,8 +45,8 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{estimatePoints &&
|
||||
estimatePoints.map((point) => (
|
||||
{activeEstimateDetails?.points &&
|
||||
activeEstimateDetails?.points?.map((point) => (
|
||||
<CustomSelect.Option key={point.key} value={point.key}>
|
||||
<>
|
||||
<span>
|
||||
@ -56,4 +58,4 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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<Props> = 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<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && isEstimateActive && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
||||
<p>Estimate</p>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||
areEstimatesEnabledForCurrentProject && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
||||
<p>Estimate</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value } }) => (
|
||||
<SidebarEstimateSelect
|
||||
value={value}
|
||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||
disabled={!isAllowed || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value } }) => (
|
||||
<SidebarEstimateSelect
|
||||
value={value}
|
||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||
disabled={!isAllowed || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showSecondSection && (
|
||||
|
@ -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<Props> = ({
|
||||
issue,
|
||||
onChange,
|
||||
tooltipPosition = "top",
|
||||
customButton = false,
|
||||
disabled,
|
||||
}) => {
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
|
||||
export const ViewEstimateSelect: React.FC<Props> = 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 = (
|
||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
|
||||
@ -36,7 +31,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (!isEstimateActive) return null;
|
||||
if (!areEstimatesEnabledForCurrentProject) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
@ -56,7 +51,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{estimatePoints?.map((estimate) => (
|
||||
{activeEstimateDetails?.points?.map((estimate) => (
|
||||
<CustomSelect.Option key={estimate.id} value={estimate.key}>
|
||||
<>
|
||||
<Triangle className="h-3 w-3" />
|
||||
@ -66,4 +61,4 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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";
|
||||
|
11
web/hooks/store/use-estimate.ts
Normal file
11
web/hooks/store/use-estimate.ts
Normal file
@ -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;
|
||||
};
|
@ -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;
|
@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
import {
|
||||
useApplication,
|
||||
useCycle,
|
||||
useEstimate,
|
||||
useLabel,
|
||||
useMember,
|
||||
useModule,
|
||||
@ -30,7 +31,6 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store
|
||||
const {
|
||||
projectEstimates: { fetchProjectEstimates },
|
||||
inbox: { fetchInboxesList, isInboxEnabled },
|
||||
} = useMobxStore();
|
||||
const {
|
||||
@ -50,6 +50,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
const {
|
||||
project: { fetchProjectLabels },
|
||||
} = useLabel();
|
||||
const { fetchProjectEstimates } = useEstimate();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
@ -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<any> {
|
||||
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) => {
|
||||
|
228
web/store/estimate.store.ts
Normal file
228
web/store/estimate.store.ts
Normal file
@ -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<string, IEstimate[] | null>;
|
||||
// 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<IEstimate[]>;
|
||||
createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>;
|
||||
updateEstimate: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string,
|
||||
data: IEstimateFormData
|
||||
) => Promise<IEstimate>;
|
||||
deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ProjectEstimatesStore implements IProjectEstimateStore {
|
||||
// states
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
// observables
|
||||
estimates: Record<string, IEstimate[] | null> = {};
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
14
web/types/estimate.d.ts
vendored
14
web/types/estimate.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user