refactor: project settings (#2575)
* refactor: project setting estimate * refactor: project setting label * refactor: project setting state * refactor: project setting integration * refactor: project settings member * fix: estimate not updating * fix: estimate not in observable * fix: build error
This commit is contained in:
parent
80e6d7e1ea
commit
2d64caef90
44 changed files with 2308 additions and 1929 deletions
|
|
@ -1,15 +1,11 @@
|
|||
import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { ProjectEstimateService } from "services/project";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
|
|
@ -17,29 +13,15 @@ import { Button, Input, TextArea } from "@plane/ui";
|
|||
// helpers
|
||||
import { checkDuplicates } from "helpers/array.helper";
|
||||
// types
|
||||
import { IUser, IEstimate, IEstimateFormData } from "types";
|
||||
// fetch-keys
|
||||
import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
|
||||
import { IEstimate, IEstimateFormData } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IEstimate;
|
||||
user: IUser | undefined;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
value1: string;
|
||||
value2: string;
|
||||
value3: string;
|
||||
value4: string;
|
||||
value5: string;
|
||||
value6: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<FormValues> = {
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
value1: "",
|
||||
|
|
@ -50,10 +32,18 @@ const defaultValues: Partial<FormValues> = {
|
|||
value6: "",
|
||||
};
|
||||
|
||||
// services
|
||||
const projectEstimateService = new ProjectEstimateService();
|
||||
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: projectEstimatesStore } = useMobxStore();
|
||||
|
||||
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen, user }) => {
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
|
|
@ -68,71 +58,47 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||
reset();
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const createEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await projectEstimateService
|
||||
.createEstimate(workspaceSlug as string, projectId as string, payload, user)
|
||||
await projectEstimatesStore
|
||||
.createEstimate(workspaceSlug.toString(), projectId.toString(), payload)
|
||||
.then(() => {
|
||||
mutate(ESTIMATES_LIST(projectId as string));
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate with that name already exists. Please try again with another name.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate could not be created. Please try again.",
|
||||
});
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
errorString ?? err.status === 400
|
||||
? "Estimate with that name already exists. Please try again with another name."
|
||||
: "Estimate could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
mutate<IEstimate[]>(
|
||||
ESTIMATES_LIST(projectId.toString()),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => {
|
||||
if (p.id === data.id)
|
||||
return {
|
||||
...p,
|
||||
name: payload.estimate.name,
|
||||
description: payload.estimate.description,
|
||||
points: p.points.map((point, index) => ({
|
||||
...point,
|
||||
value: payload.estimate_points[index].value,
|
||||
})),
|
||||
};
|
||||
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await projectEstimateService
|
||||
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload, user)
|
||||
await projectEstimatesStore
|
||||
.updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload)
|
||||
.then(() => {
|
||||
mutate(ESTIMATES_LIST(projectId.toString()));
|
||||
mutate(ESTIMATE_DETAILS(data.id));
|
||||
handleClose();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate could not be updated. Please try again.",
|
||||
message: errorString ?? "Estimate could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -291,151 +257,38 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* list of all the points */}
|
||||
{/* since they are all the same, we can use a loop to render them */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">1</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value1"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value1"
|
||||
name="value1"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value1)}
|
||||
placeholder="Point 1"
|
||||
className="rounded-l-none w-full"
|
||||
{Array(6)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`value${i + 1}` as keyof FormValues}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
id={`value${i + 1}`}
|
||||
name={`value${i + 1}`}
|
||||
placeholder={`Point ${i + 1}`}
|
||||
className="rounded-l-none w-full"
|
||||
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">2</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value2"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value2"
|
||||
name="value2"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value2)}
|
||||
placeholder="Point 2"
|
||||
className="rounded-l-none w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">3</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value3"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value3"
|
||||
name="value3"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value3)}
|
||||
placeholder="Point 3"
|
||||
className="rounded-l-none w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">4</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value4"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value4"
|
||||
name="value4"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value4)}
|
||||
placeholder="Point 4"
|
||||
className="rounded-l-none w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">5</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value5"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value5"
|
||||
name="value5"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value5)}
|
||||
placeholder="Point 5"
|
||||
className="rounded-l-none w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">6</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value6"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="value6"
|
||||
name="value6"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.value6)}
|
||||
placeholder="Point 6"
|
||||
className="rounded-l-none w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
|
|
@ -461,4 +314,4 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// headless ui
|
||||
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 useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IEstimate } from "types";
|
||||
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
|
|
@ -12,14 +15,43 @@ import { Button } from "@plane/ui";
|
|||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
data: IEstimate | null;
|
||||
handleClose: () => void;
|
||||
data: IEstimate;
|
||||
handleDelete: () => void;
|
||||
};
|
||||
|
||||
export const DeleteEstimateModal: React.FC<Props> = ({ isOpen, handleClose, data, handleDelete }) => {
|
||||
export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, handleClose, data } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { projectEstimates: projectEstimatesStore } = useMobxStore();
|
||||
|
||||
// states
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleEstimateDelete = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const estimateId = data?.id!;
|
||||
|
||||
projectEstimatesStore.deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId).catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be deleted. Please try again",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
|
|
@ -68,7 +100,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({ isOpen, handleClose, data
|
|||
<span>
|
||||
<p className="break-words text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete estimate-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data.name}</span>
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>
|
||||
{""}? All of the data related to the estiamte will be permanently removed. This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
|
|
@ -81,7 +113,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({ isOpen, handleClose, data
|
|||
variant="danger"
|
||||
onClick={() => {
|
||||
setIsDeleteLoading(true);
|
||||
handleDelete();
|
||||
handleEstimateDelete();
|
||||
}}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
|
|
@ -96,4 +128,4 @@ export const DeleteEstimateModal: React.FC<Props> = ({ isOpen, handleClose, data
|
|||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// services
|
||||
import { ProjectService } from "services/project";
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { DeleteEstimateModal } from "components/estimates";
|
||||
// ui
|
||||
import { Button, CustomMenu } from "@plane/ui";
|
||||
//icons
|
||||
|
|
@ -16,48 +14,46 @@ import { Pencil, Trash2 } from "lucide-react";
|
|||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IUser, IEstimate } from "types";
|
||||
import { IEstimate } from "types";
|
||||
|
||||
type Props = {
|
||||
user: IUser | undefined;
|
||||
estimate: IEstimate;
|
||||
editEstimate: (estimate: IEstimate) => void;
|
||||
handleEstimateDelete: (estimateId: string) => void;
|
||||
deleteEstimate: (estimateId: string) => void;
|
||||
};
|
||||
|
||||
// services
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export const SingleEstimate: React.FC<Props> = ({ user, estimate, editEstimate, handleEstimateDelete }) => {
|
||||
const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false);
|
||||
export const EstimateListItem: React.FC<Props> = observer((props) => {
|
||||
const { estimate, editEstimate, deleteEstimate } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails, mutateProjectDetails } = useProjectDetails();
|
||||
// derived values
|
||||
const projectDetails = projectStore.project_details?.[projectId?.toString()!];
|
||||
|
||||
const handleUseEstimate = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const payload = {
|
||||
estimate: estimate.id,
|
||||
};
|
||||
await projectStore
|
||||
.updateProject(workspaceSlug.toString(), projectId.toString(), {
|
||||
estimate: estimate.id,
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
mutateProjectDetails((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return { ...prevData, estimate: estimate.id };
|
||||
}, false);
|
||||
|
||||
await projectService.updateProject(workspaceSlug as string, projectId as string, payload, user).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate points could not be used. Please try again.",
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate points could not be used. Please try again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -76,7 +72,7 @@ export const SingleEstimate: React.FC<Props> = ({ user, estimate, editEstimate,
|
|||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && (
|
||||
{projectDetails?.estimate !== estimate?.id && estimate?.points?.length > 0 && (
|
||||
<Button variant="neutral-primary" onClick={handleUseEstimate}>
|
||||
Use
|
||||
</Button>
|
||||
|
|
@ -95,7 +91,7 @@ export const SingleEstimate: React.FC<Props> = ({ user, estimate, editEstimate,
|
|||
{projectDetails?.estimate !== estimate.id && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteEstimateModalOpen(true);
|
||||
deleteEstimate(estimate.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
|
|
@ -107,7 +103,7 @@ export const SingleEstimate: React.FC<Props> = ({ user, estimate, editEstimate,
|
|||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
{estimate.points.length > 0 ? (
|
||||
{estimate?.points?.length > 0 ? (
|
||||
<div className="flex text-xs text-custom-text-200">
|
||||
Estimate points (
|
||||
<span className="flex gap-1">
|
||||
|
|
@ -126,16 +122,6 @@ export const SingleEstimate: React.FC<Props> = ({ user, estimate, editEstimate,
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeleteEstimateModal
|
||||
isOpen={isDeleteEstimateModalOpen}
|
||||
handleClose={() => setIsDeleteEstimateModalOpen(false)}
|
||||
data={estimate}
|
||||
handleDelete={() => {
|
||||
handleEstimateDelete(estimate.id);
|
||||
setIsDeleteEstimateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
139
web/components/estimates/estimate-list.tsx
Normal file
139
web/components/estimates/estimate-list.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// 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: projectStore } = useMobxStore();
|
||||
|
||||
// states
|
||||
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
|
||||
const [estimateToDelete, setEstimateToDelete] = useState<string | null>(null);
|
||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
||||
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// derived values
|
||||
const estimatesList = projectStore.projectEstimates;
|
||||
const projectDetails = projectStore.project_details?.[projectId?.toString()!];
|
||||
|
||||
const editEstimate = (estimate: IEstimate) => {
|
||||
setEstimateFormOpen(true);
|
||||
setEstimateToUpdate(estimate);
|
||||
};
|
||||
|
||||
const disableEstimates = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
projectStore.updateProject(workspaceSlug.toString(), projectId.toString(), { estimate: null }).catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be disabled. Please try again",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateEstimateModal
|
||||
isOpen={estimateFormOpen}
|
||||
data={estimateToUpdate}
|
||||
handleClose={() => {
|
||||
setEstimateFormOpen(false);
|
||||
setEstimateToUpdate(undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteEstimateModal
|
||||
isOpen={!!estimateToDelete}
|
||||
handleClose={() => setEstimateToDelete(null)}
|
||||
data={projectStore.getProjectEstimateById(estimateToDelete!)}
|
||||
/>
|
||||
|
||||
<section className="flex items-center justify-between py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Estimates</h3>
|
||||
<div className="col-span-12 space-y-5 sm:col-span-7">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setEstimateFormOpen(true);
|
||||
setEstimateToUpdate(undefined);
|
||||
}}
|
||||
>
|
||||
Add Estimate
|
||||
</Button>
|
||||
{projectDetails?.estimate && (
|
||||
<Button variant="neutral-primary" onClick={disableEstimates}>
|
||||
Disable Estimates
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{estimatesList ? (
|
||||
estimatesList.length > 0 ? (
|
||||
<section className="h-full bg-custom-background-100 overflow-y-auto">
|
||||
{estimatesList.map((estimate) => (
|
||||
<EstimateListItem
|
||||
key={estimate.id}
|
||||
estimate={estimate}
|
||||
editEstimate={(estimate) => editEstimate(estimate)}
|
||||
deleteEstimate={(estimateId) => setEstimateToDelete(estimateId)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<EmptyState
|
||||
title="No estimates yet"
|
||||
description="Estimates help you communicate the complexity of an issue."
|
||||
image={emptyEstimate}
|
||||
primaryButton={{
|
||||
icon: <Plus className="h-4 w-4" />,
|
||||
text: "Add Estimate",
|
||||
onClick: () => {
|
||||
setEstimateFormOpen(true);
|
||||
setEstimateToUpdate(undefined);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-5 space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export * from "./create-update-estimate-modal";
|
||||
export * from "./delete-estimate-modal";
|
||||
export * from "./estimate-select";
|
||||
export * from "./single-estimate";
|
||||
export * from "./estimate-list-item";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue