feat: estimates (#783)

* chore: use estimate points hook created

* chore: user auth layer

* fix: build error

* chore: estimate crud and validation

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
Kunal Vishwakarma 2023-04-11 17:54:01 +05:30 committed by GitHub
parent d5c2965946
commit dfa3a7b78d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 225 additions and 255 deletions

View File

@ -28,9 +28,7 @@ export const NotAWorkspaceMember = () => {
<div className="flex items-center gap-2 justify-center"> <div className="flex items-center gap-2 justify-center">
<Link href="/invitations"> <Link href="/invitations">
<a> <a>
<SecondaryButton onClick={() => router.back()}> <SecondaryButton>Check pending invites</SecondaryButton>
Check pending invites
</SecondaryButton>
</a> </a>
</Link> </Link>
<Link href="/create-workspace"> <Link href="/create-workspace">

View File

@ -20,6 +20,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { Properties } from "types"; import { Properties } from "types";
// constants // constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useEstimateOption from "hooks/use-estimate-option";
export const IssuesFilterView: React.FC = () => { export const IssuesFilterView: React.FC = () => {
const router = useRouter(); const router = useRouter();
@ -45,6 +46,8 @@ export const IssuesFilterView: React.FC = () => {
projectId as string projectId as string
); );
const { isEstimateActive } = useEstimateOption();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
@ -233,20 +236,24 @@ export const IssuesFilterView: React.FC = () => {
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<h4 className="text-sm text-gray-600">Display Properties</h4> <h4 className="text-sm text-gray-600">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => ( {Object.keys(properties).map((key) => {
<button if (key === "estimate" && !isEstimateActive) return null;
key={key}
type="button" return (
className={`rounded border px-2 py-1 text-xs capitalize ${ <button
properties[key as keyof Properties] key={key}
? "border-theme bg-theme text-white" type="button"
: "border-gray-300" className={`rounded border px-2 py-1 text-xs capitalize ${
}`} properties[key as keyof Properties]
onClick={() => setProperties(key as keyof Properties)} ? "border-theme bg-theme text-white"
> : "border-gray-300"
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)} }`}
</button> onClick={() => setProperties(key as keyof Properties)}
))} >
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@ import useToast from "hooks/use-toast";
import { import {
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect,
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues/view-select"; } from "components/issues/view-select";
@ -273,6 +274,14 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{type && !isNotAllowed && ( {type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis> <CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}> <CustomMenu.MenuItem onClick={editIssue}>

View File

@ -21,10 +21,9 @@ import { IEstimate } from "types";
import { ESTIMATES_LIST } from "constants/fetch-keys"; import { ESTIMATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean;
handleClose: () => void; handleClose: () => void;
data?: IEstimate; data?: IEstimate;
isOpen: boolean;
isCreate: boolean;
}; };
const defaultValues: Partial<IEstimate> = { const defaultValues: Partial<IEstimate> = {
@ -32,7 +31,7 @@ const defaultValues: Partial<IEstimate> = {
description: "", description: "",
}; };
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen, isCreate }) => { export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -109,12 +108,11 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
}; };
useEffect(() => { useEffect(() => {
if (!data && isCreate) return;
reset({ reset({
...defaultValues, ...defaultValues,
...data, ...data,
}); });
}, [data, reset, isCreate]); }, [data, reset]);
return ( return (
<> <>
@ -148,7 +146,9 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
onSubmit={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)} onSubmit={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
> >
<div className="space-y-3"> <div className="space-y-3">
<div className="text-2xl font-medium">Create Estimate</div> <div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate
</div>
<div> <div>
<Input <Input
id="name" id="name"

View File

@ -18,6 +18,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import type { IEstimate, IEstimatePoint } from "types"; import type { IEstimate, IEstimatePoint } from "types";
import estimatesService from "services/estimates.service"; import estimatesService from "services/estimates.service";
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -33,8 +34,6 @@ interface FormValues {
value4: string; value4: string;
value5: string; value5: string;
value6: string; value6: string;
value7: string;
value8: string;
} }
const defaultValues: FormValues = { const defaultValues: FormValues = {
@ -44,8 +43,6 @@ const defaultValues: FormValues = {
value4: "", value4: "",
value5: "", value5: "",
value6: "", value6: "",
value7: "",
value8: "",
}; };
export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, onClose }) => { export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, onClose }) => {
@ -56,7 +53,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { isSubmitting },
handleSubmit, handleSubmit,
reset, reset,
} = useForm<FormValues>({ defaultValues }); } = useForm<FormValues>({ defaultValues });
@ -95,14 +92,6 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
key: 5, key: 5,
value: formData.value6, value: formData.value6,
}, },
{
key: 6,
value: formData.value7,
},
{
key: 7,
value: formData.value8,
},
], ],
}; };
@ -126,49 +115,20 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
}; };
const updateEstimatePoints = async (formData: FormValues) => { const updateEstimatePoints = async (formData: FormValues) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !data || data.length === 0) return;
const payload = { const payload = {
estimate_points: [ estimate_points: data.map((d, index) => ({
{ id: d.id,
key: 0, value: (formData as any)[`value${index + 1}`],
value: formData.value1, })),
},
{
key: 1,
value: formData.value2,
},
{
key: 2,
value: formData.value3,
},
{
key: 3,
value: formData.value4,
},
{
key: 4,
value: formData.value5,
},
{
key: 5,
value: formData.value6,
},
{
key: 6,
value: formData.value7,
},
{
key: 7,
value: formData.value8,
},
],
}; };
await estimatesService await estimatesService
.updateEstimatesPoints( .patchEstimatePoints(
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
estimate?.id as string, estimate?.id as string,
data?.[0]?.id as string,
payload payload
) )
.then(() => { .then(() => {
@ -183,15 +143,27 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
}); });
}; };
const onSubmit = async (formData: FormValues) => {
if (data && data.length !== 0) await updateEstimatePoints(formData);
else await createEstimatePoints(formData);
if (estimate) mutate(ESTIMATE_POINTS_LIST(estimate.id));
};
useEffect(() => { useEffect(() => {
if(!data) return if (!data || data.length < 6) return;
reset({ reset({
...defaultValues, ...defaultValues,
...data, value1: data[0].value,
value2: data[1].value,
value3: data[2].value,
value4: data[3].value,
value5: data[4].value,
value6: data[5].value,
}); });
}, [data, reset]); }, [data, reset]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}> <Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
@ -219,18 +191,16 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form <form onSubmit={handleSubmit(onSubmit)}>
onSubmit={
data ? handleSubmit(updateEstimatePoints) : handleSubmit(createEstimatePoints)
}
>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h4 className="text-2xl font-medium">Create Estimate Points</h4> <h4 className="text-lg font-medium leading-6">
<div className="grid grid-cols-4 gap-3"> {data ? "Update" : "Create"} Estimate Points
</h4>
<div className="grid grid-cols-3 gap-3">
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V0</span> <span className="px-2 rounded-lg text-sm text-gray-600">1</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -243,7 +213,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
required: "value is required", required: "value is required",
maxLength: { maxLength: {
value: 10, value: 10,
message: "value should be less than 10 characters", message: "Name should be less than 10 characters",
}, },
}} }}
/> />
@ -252,7 +222,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V1</span> <span className="px-2 rounded-lg text-sm text-gray-600">2</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -274,7 +244,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V2</span> <span className="px-2 rounded-lg text-sm text-gray-600">3</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -296,7 +266,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V3</span> <span className="px-2 rounded-lg text-sm text-gray-600">4</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -318,7 +288,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V4</span> <span className="px-2 rounded-lg text-sm text-gray-600">5</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -340,7 +310,7 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg"> <span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V5</span> <span className="px-2 rounded-lg text-sm text-gray-600">6</span>
<span className="bg-white rounded-lg"> <span className="bg-white rounded-lg">
<Input <Input
id="name" id="name"
@ -360,50 +330,6 @@ export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, o
</span> </span>
</span> </span>
</div> </div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V6</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value7"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="pl-2 pr-1 rounded-lg text-sm text-gray-600">V7</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value8"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 20,
message: "Name should be less than 20 characters",
},
}}
/>
</span>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,6 +25,7 @@ import {
import { IEstimate, IProject } from "types"; import { IEstimate, IProject } from "types";
// fetch-keys // fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
import { orderArrayBy } from "helpers/array.helper";
type Props = { type Props = {
estimate: IEstimate; estimate: IEstimate;
@ -88,6 +89,7 @@ export const SingleEstimate: React.FC<Props> = ({
isOpen={isEstimatePointsModalOpen} isOpen={isEstimatePointsModalOpen}
estimate={estimate} estimate={estimate}
onClose={() => setIsEstimatePointsModalOpen(false)} onClose={() => setIsEstimatePointsModalOpen(false)}
data={estimatePoints ? orderArrayBy(estimatePoints, "key") : undefined}
/> />
<div className="gap-2 py-3"> <div className="gap-2 py-3">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -105,7 +107,7 @@ export const SingleEstimate: React.FC<Props> = ({
</p> </p>
</div> </div>
<CustomMenu ellipsis> <CustomMenu ellipsis>
{projectDetails?.estimate && projectDetails?.estimate !== estimate.id && ( {projectDetails?.estimate !== estimate.id && (
<CustomMenu.MenuItem onClick={handleUseEstimate}> <CustomMenu.MenuItem onClick={handleUseEstimate}>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<SquaresPlusIcon className="h-3.5 w-3.5" /> <SquaresPlusIcon className="h-3.5 w-3.5" />
@ -116,7 +118,9 @@ export const SingleEstimate: React.FC<Props> = ({
<CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}> <CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<ListBulletIcon className="h-3.5 w-3.5" /> <ListBulletIcon className="h-3.5 w-3.5" />
<span>{estimatePoints?.length === 8 ? "Update points" : "Create points"}</span> <span>
{estimatePoints && estimatePoints?.length > 0 ? "Edit points" : "Create points"}
</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem <CustomMenu.MenuItem
@ -129,32 +133,34 @@ export const SingleEstimate: React.FC<Props> = ({
<span>Edit estimate</span> <span>Edit estimate</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem {projectDetails?.estimate !== estimate.id && (
onClick={() => { <CustomMenu.MenuItem
handleEstimateDelete(estimate.id); onClick={() => {
}} handleEstimateDelete(estimate.id);
> }}
<div className="flex items-center justify-start gap-2"> >
<TrashIcon className="h-3.5 w-3.5" /> <div className="flex items-center justify-start gap-2">
<span>Delete estimate</span> <TrashIcon className="h-3.5 w-3.5" />
</div> <span>Delete estimate</span>
</CustomMenu.MenuItem> </div>
</CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
</div> </div>
{estimatePoints && estimatePoints.length > 0 ? ( {estimatePoints && estimatePoints.length > 0 ? (
<div className="flex gap-2"> <div className="flex gap-2 text-sm text-gray-400">
{estimatePoints.length > 0 && "Estimate points ("} Estimate points(
{estimatePoints.map((point, i) => ( {estimatePoints.map((point, index) => (
<h6 key={point.id}> <h6 key={point.id}>
{point.value} {point.value}
{i !== estimatePoints.length - 1 && ","}{" "} {index !== estimatePoints.length - 1 && ","}{" "}
</h6> </h6>
))} ))}
{estimatePoints.length > 0 && ")"} )
</div> </div>
) : ( ) : (
<div> <div>
<p className=" text-sm text-gray-300">No estimate points</p> <p className="text-sm text-gray-400">No estimate points</p>
</div> </div>
)} )}
</div> </div>

View File

@ -405,7 +405,7 @@ export const IssueForm: FC<IssueFormProps> = ({
control={control} control={control}
name="estimate_point" name="estimate_point"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<IssueEstimateSelect chevron={false} value={value} onChange={onChange} /> <IssueEstimateSelect value={value} onChange={onChange} />
)} )}
/> />
</div> </div>

View File

@ -1,72 +1,37 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
import estimatesService from "services/estimates.service";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { PlayIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/outline";
// fetch-keys // fetch-keys
import { ESTIMATE_POINTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import useEstimateOption from "hooks/use-estimate-option";
type Props = { type Props = {
value: number; value: number;
onChange: (value: number) => void; onChange: (value: number) => void;
chevron: boolean;
}; };
export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange, chevron }) => { export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter(); const { isEstimateActive, estimatePoints } = useEstimateOption();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR( if (!isEstimateActive) return null;
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
const { data: estimatePoints } = useSWR(
workspaceSlug && projectId && projectDetails && projectDetails.estimate
? ESTIMATE_POINTS_LIST(projectDetails.estimate)
: null,
workspaceSlug && projectId && projectDetails && projectDetails.estimate
? () =>
estimatesService.getEstimatesPointsList(
workspaceSlug as string,
projectId as string,
projectDetails.estimate
)
: null
);
return ( return (
<CustomSelect <CustomSelect
value={value} value={value}
label={ label={
<div className="flex items-center gap-2 text-xs min-w-[calc(100%+10px)]"> <div className="flex items-center gap-2 text-xs">
<span> <PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" />
<PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" />
</span>
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}> <span className={`${value ? "text-gray-600" : "text-gray-500"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate points"} {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate points"}
</span> </span>
{chevron && (
<span className="w-full flex justify-end pr-3">
<ChevronDownIcon className="h-[9px] w-[9px] text-black" />
</span>
)}
</div> </div>
} }
onChange={onChange} onChange={onChange}
position="right" position="right"
width="w-full min-w-[111px]" width="w-full min-w-[6rem]"
noChevron={!chevron} noChevron
> >
{estimatePoints && {estimatePoints &&
estimatePoints.map((point) => ( estimatePoints.map((point) => (

View File

@ -1,13 +1,12 @@
import React from "react"; import React from "react";
// ui // ui
import { IssueEstimateSelect } from "components/issues/select"; import { CustomSelect } from "components/ui";
// icons // icons
import { BanknotesIcon } from "@heroicons/react/24/outline"; import { BanknotesIcon, PlayIcon } from "@heroicons/react/24/outline";
// types // types
import { UserAuth } from "types"; import { UserAuth } from "types";
import useEstimateOption from "hooks/use-estimate-option";
// constants // constants
type Props = { type Props = {
@ -16,19 +15,47 @@ type Props = {
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => { export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const { isEstimateActive, estimatePoints } = useEstimateOption();
if (!isEstimateActive) return null;
return ( return (
<div className="flex flex-wrap items-center py-2"> <div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2"> <div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<BanknotesIcon className="h-4 w-4 flex-shrink-0" /> <PlayIcon className="h-4 w-4 -rotate-90 flex-shrink-0" />
<p>Estimate</p> <p>Estimate</p>
</div> </div>
<div className="sm:basis-1/2"> <div className="sm:basis-1/2">
<IssueEstimateSelect chevron={true} value={value} onChange={onChange} /> <CustomSelect
value={value}
label={
<div className="flex items-center gap-2 text-xs">
<PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" />
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate points"}
</span>
</div>
}
onChange={onChange}
position="right"
width="w-full"
disabled={isNotAllowed}
>
{estimatePoints &&
estimatePoints.map((point) => (
<CustomSelect.Option className="w-full " key={point.key} value={point.key}>
<>
<span>
<PlayIcon className="h-4 w-4 -rotate-90" />
</span>
{point.value}
</>
</CustomSelect.Option>
))}
</CustomSelect>
</div> </div>
</div> </div>
); );

View File

@ -11,6 +11,7 @@ import { PRIORITIES } from "constants/project";
// services // services
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
import useEstimateOption from "hooks/use-estimate-option"; import useEstimateOption from "hooks/use-estimate-option";
import { PlayIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
@ -27,15 +28,17 @@ export const ViewEstimateSelect: React.FC<Props> = ({
selfPositioned = false, selfPositioned = false,
isNotAllowed, isNotAllowed,
}) => { }) => {
const { isEstimateActive, estimatePoints, estimateValue } = useEstimateOption( const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
issue.estimate_point
); const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
if (!isEstimateActive) return null;
return ( return (
<CustomSelect <CustomSelect
value={issue.priority} value={issue.estimate_point}
onChange={(data: string) => { onChange={(val: number) => {
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date }); partialUpdateIssue({ estimate_point: val });
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug: issue.workspace_detail.slug, workspaceSlug: issue.workspace_detail.slug,
@ -45,12 +48,15 @@ export const ViewEstimateSelect: React.FC<Props> = ({
projectName: issue.project_detail.name, projectName: issue.project_detail.name,
issueId: issue.id, issueId: issue.id,
}, },
"ISSUE_PROPERTY_UPDATE_PRIORITY" "ISSUE_PROPERTY_UPDATE_ESTIMATE"
); );
}} }}
label={ label={
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}> <Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
<>{estimateValue}</> <div className="flex items-center gap-1 text-gray-500">
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
{estimateValue}
</div>
</Tooltip> </Tooltip>
} }
maxHeight="md" maxHeight="md"
@ -58,6 +64,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
disabled={isNotAllowed} disabled={isNotAllowed}
position={position} position={position}
selfPositioned={selfPositioned} selfPositioned={selfPositioned}
width="w-full min-w-[6rem]"
> >
{estimatePoints?.map((estimate) => ( {estimatePoints?.map((estimate) => (
<CustomSelect.Option key={estimate.id} value={estimate.key} className="capitalize"> <CustomSelect.Option key={estimate.id} value={estimate.key} className="capitalize">

View File

@ -29,7 +29,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
<CustomSelect <CustomSelect
value={issue.priority} value={issue.priority}
onChange={(data: string) => { onChange={(data: string) => {
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date }); partialUpdateIssue({ priority: data });
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug: issue.workspace_detail.slug, workspaceSlug: issue.workspace_detail.slug,

View File

@ -8,6 +8,8 @@ import useSWR from "swr";
import estimatesService from "services/estimates.service"; import estimatesService from "services/estimates.service";
// hooks // hooks
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// fetch-keys // fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
@ -26,7 +28,7 @@ const useEstimateOption = (estimateKey?: number) => {
estimatesService.getEstimatesPointsList( estimatesService.getEstimatesPointsList(
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
projectDetails.estimate projectDetails.estimate as string
) )
: null : null
); );
@ -41,7 +43,7 @@ const useEstimateOption = (estimateKey?: number) => {
return { return {
isEstimateActive: projectDetails?.estimate ? true : false, isEstimateActive: projectDetails?.estimate ? true : false,
estimatePoints, estimatePoints: orderArrayBy(estimatePoints ?? [], "key"),
estimateValue, estimateValue,
}; };
}; };

View File

@ -10,6 +10,7 @@ import Container from "layouts/container";
import AppHeader from "layouts/app-layout/app-header"; import AppHeader from "layouts/app-layout/app-header";
import AppSidebar from "layouts/app-layout/app-sidebar"; import AppSidebar from "layouts/app-layout/app-sidebar";
import SettingsNavbar from "layouts/settings-navbar"; import SettingsNavbar from "layouts/settings-navbar";
import { WorkspaceAuthorizationLayout } from "./workspace-authorization-wrapper";
// components // components
import { NotAuthorizedView, JoinProject } from "components/auth-screens"; import { NotAuthorizedView, JoinProject } from "components/auth-screens";
import { CommandPalette } from "components/command-palette"; import { CommandPalette } from "components/command-palette";
@ -89,6 +90,22 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
</PrimaryButton> </PrimaryButton>
</div> </div>
</div> </div>
) : error?.status === 401 || error?.status === 403 ? (
<JoinProject />
) : error?.status === 404 ? (
<div className="container h-screen grid place-items-center">
<div className="text-center space-y-4">
<p className="text-2xl font-semibold">No such project exist. Create one?</p>
<PrimaryButton
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
Create project
</PrimaryButton>
</div>
</div>
) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( ) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView <NotAuthorizedView
actionButton={ actionButton={

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -15,20 +15,20 @@ import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates"
//hooks //hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Loader } from "components/ui"; import { Loader, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// types // types
import { IEstimate } from "types"; import { IEstimate, IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST } from "constants/fetch-keys"; import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import projectService from "services/project.service";
const EstimatesSettings: NextPage = () => { const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateFormOpen, setEstimateFormOpen] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>(); const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
const router = useRouter(); const router = useRouter();
@ -38,8 +38,6 @@ const EstimatesSettings: NextPage = () => {
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
const scrollToRef = useRef<HTMLDivElement>(null);
const { data: estimatesList } = useSWR<IEstimate[]>( const { data: estimatesList } = useSWR<IEstimate[]>(
workspaceSlug && projectId ? ESTIMATES_LIST(projectId as string) : null, workspaceSlug && projectId ? ESTIMATES_LIST(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
@ -48,7 +46,6 @@ const EstimatesSettings: NextPage = () => {
); );
const editEstimate = (estimate: IEstimate) => { const editEstimate = (estimate: IEstimate) => {
setIsUpdating(true);
setEstimateToUpdate(estimate); setEstimateToUpdate(estimate);
setEstimateFormOpen(true); setEstimateFormOpen(true);
}; };
@ -73,6 +70,30 @@ const EstimatesSettings: NextPage = () => {
}); });
}; };
const disableEstimates = () => {
if (!workspaceSlug || !projectId) return;
mutate<IProject>(
PROJECT_DETAILS(projectId as string),
(prevData) => {
if (!prevData) return prevData;
return { ...prevData, estimate: null };
},
false
);
projectService
.updateProject(workspaceSlug as string, projectId as string, { estimate: null })
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate could not be disabled. Please try again",
})
);
};
return ( return (
<> <>
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
@ -87,7 +108,6 @@ const EstimatesSettings: NextPage = () => {
} }
> >
<CreateUpdateEstimateModal <CreateUpdateEstimateModal
isCreate={estimateToUpdate ? true : false}
isOpen={estimateFormOpen} isOpen={estimateFormOpen}
data={estimateToUpdate} data={estimateToUpdate}
handleClose={() => { handleClose={() => {
@ -95,14 +115,12 @@ const EstimatesSettings: NextPage = () => {
setEstimateToUpdate(undefined); setEstimateToUpdate(undefined);
}} }}
/> />
<section className="grid grid-cols-12 gap-10"> <section className="flex items-center justify-between">
<div className="col-span-12 sm:col-span-5"> <h3 className="text-2xl font-semibold">Estimates</h3>
<h3 className="text-[28px] font-semibold">Estimates</h3>
</div>
<div className="col-span-12 space-y-5 sm:col-span-7"> <div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex sm:justify-end sm:items-end sm:h-full text-theme"> <div className="flex items-center gap-2">
<span <span
className="flex items-center cursor-pointer gap-2" className="flex items-center cursor-pointer gap-2 text-theme"
onClick={() => { onClick={() => {
setEstimateToUpdate(undefined); setEstimateToUpdate(undefined);
setEstimateFormOpen(true); setEstimateFormOpen(true);
@ -111,6 +129,9 @@ const EstimatesSettings: NextPage = () => {
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Create New Estimate Create New Estimate
</span> </span>
{projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
)}
</div> </div>
</div> </div>
</section> </section>

View File

@ -22,9 +22,9 @@ import { ONBOARDING_CARDS } from "constants/workspace";
import Logo from "public/onboarding/logo.svg"; import Logo from "public/onboarding/logo.svg";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { ICurrentUserResponse } from "types";
// fetch-keys // fetch-keys
import { CURRENT_USER } from "constants/fetch-keys"; import { CURRENT_USER } from "constants/fetch-keys";
import { ICurrentUserResponse } from "types";
const Onboarding: NextPage = () => { const Onboarding: NextPage = () => {
const [step, setStep] = useState(1); const [step, setStep] = useState(1);

View File

@ -1,6 +1,6 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
// types
import type { IEstimate, IEstimatePoint } from "types"; import type { IEstimate, IEstimatePoint } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -78,7 +78,7 @@ class ProjectEstimateServices extends APIService {
} }
): Promise<any> { ): Promise<any> {
return this.post( return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/bulk-create-estimate-points/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/bulk-estimate-points/`,
data data
) )
.then((response) => response?.data) .then((response) => response?.data)
@ -87,7 +87,7 @@ class ProjectEstimateServices extends APIService {
}); });
} }
async getEstimatesPoints( async getEstimatesPointDetails(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
estimateId: string, estimateId: string,
@ -116,15 +116,14 @@ class ProjectEstimateServices extends APIService {
}); });
} }
async updateEstimatesPoints( async patchEstimatePoints(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
estimateId: string, estimateId: string,
estimatePointId: string,
data: any data: any
): Promise<any> { ): Promise<any> {
return this.patch( return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimate/${estimateId}/estimate-points/${estimatePointId}`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/bulk-estimate-points/`,
data data
) )
.then((response) => response?.data) .then((response) => response?.data)
@ -132,21 +131,6 @@ class ProjectEstimateServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async deleteEstimatesPoints(
workspaceSlug: string,
projectId: string,
estimateId: string,
estimatePointId: string
): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimate/${estimateId}/estimate-points/${estimatePointId}`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }
export default new ProjectEstimateServices(); export default new ProjectEstimateServices();

View File

@ -192,6 +192,7 @@ class TrackEventServices extends APIService {
| "ISSUE_PROPERTY_UPDATE_STATE" | "ISSUE_PROPERTY_UPDATE_STATE"
| "ISSUE_PROPERTY_UPDATE_ASSIGNEE" | "ISSUE_PROPERTY_UPDATE_ASSIGNEE"
| "ISSUE_PROPERTY_UPDATE_DUE_DATE" | "ISSUE_PROPERTY_UPDATE_DUE_DATE"
| "ISSUE_PROPERTY_UPDATE_ESTIMATE"
): Promise<any> { ): Promise<any> {
if (!trackEvent) return; if (!trackEvent) return;
return this.request({ return this.request({

View File

@ -18,7 +18,7 @@ export interface IProject {
page_view: boolean; page_view: boolean;
default_assignee: IUser | string | null; default_assignee: IUser | string | null;
description: string; description: string;
estimate: string; estimate: string | null;
icon: string; icon: string;
id: string; id: string;
identifier: string; identifier: string;