mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: updated create estimate and handled the build error
This commit is contained in:
parent
bc1702647c
commit
3800f4366c
6
packages/types/src/estimate.d.ts
vendored
6
packages/types/src/estimate.d.ts
vendored
@ -31,13 +31,11 @@ export interface IEstimate {
|
||||
}
|
||||
|
||||
export interface IEstimateFormData {
|
||||
estimate: {
|
||||
name: string;
|
||||
description: string;
|
||||
estimate?: {
|
||||
type: string;
|
||||
};
|
||||
estimate_points: {
|
||||
id?: string;
|
||||
id?: string | undefined;
|
||||
key: number;
|
||||
value: string;
|
||||
}[];
|
||||
|
@ -29,7 +29,7 @@
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"sonner": "^1.4.2",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -14,7 +14,7 @@ type RadioInputProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const RadioInput = ({
|
||||
export const RadioInput = ({
|
||||
label: inputLabel,
|
||||
labelClassName: inputLabelClassName,
|
||||
options,
|
||||
@ -63,4 +63,4 @@ const RadioInput = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { RadioInput };
|
||||
export default RadioInput;
|
||||
|
@ -21,7 +21,7 @@ const moveItems = <T,>(data: T[], source: T, destination: T): T[] => {
|
||||
return newData;
|
||||
};
|
||||
|
||||
const Sortable = <T,>({ data, render, onChange, keyExtractor }: Props<T>) => {
|
||||
export const Sortable = <T,>({ data, render, onChange, keyExtractor }: Props<T>) => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = monitorForElements({
|
||||
onDrop({ source, location }) {
|
||||
@ -46,4 +46,4 @@ const Sortable = <T,>({ data, render, onChange, keyExtractor }: Props<T>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { Sortable };
|
||||
export default Sortable;
|
||||
|
@ -24,7 +24,10 @@ import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon }
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
import { useEstimate, useLabel } from "@/hooks/store";
|
||||
import {
|
||||
// useEstimate,
|
||||
useLabel,
|
||||
} from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
|
||||
@ -97,21 +100,21 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works
|
||||
);
|
||||
});
|
||||
|
||||
const EstimatePoint = observer((props: { point: string }) => {
|
||||
const { point } = props;
|
||||
const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
const currentPoint = Number(point) + 1;
|
||||
// const EstimatePoint = observer((props: { point: string }) => {
|
||||
// const { point } = props;
|
||||
// const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
// const currentPoint = Number(point) + 1;
|
||||
|
||||
const estimateValue = getEstimatePointValue(Number(point), null);
|
||||
// const estimateValue = getEstimatePointValue(Number(point), null);
|
||||
|
||||
return (
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">
|
||||
{areEstimatesEnabledForCurrentProject
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
// return (
|
||||
// <span className="font-medium text-custom-text-100 whitespace-nowrap">
|
||||
// {areEstimatesEnabledForCurrentProject
|
||||
// ? estimateValue
|
||||
// : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
// </span>
|
||||
// );
|
||||
// });
|
||||
|
||||
const inboxActivityMessage = {
|
||||
declined: {
|
||||
@ -267,7 +270,8 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to <EstimatePoint point={activity.new_value} />
|
||||
set the estimate point to
|
||||
{/* <EstimatePoint point={activity.new_value} /> */}
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState, useRef, Fragment } from "react";
|
||||
import React, { useEffect, useState, useRef, Fragment, Ref } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form"; // services
|
||||
import { usePopper } from "react-popper";
|
||||
// ui
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Popover, PopoverButton, PopoverPanel, Transition } from "@headlessui/react";
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
// icons
|
||||
@ -178,9 +178,9 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Popover as="div" className={`relative w-min text-left`}>
|
||||
<Popover.Button as={Fragment}>
|
||||
<PopoverButton as={Fragment}>
|
||||
<button ref={setReferenceElement}>{button}</button>
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
as={React.Fragment}
|
||||
@ -191,10 +191,10 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel
|
||||
<PopoverPanel
|
||||
as="div"
|
||||
className={`fixed z-10 flex w-full min-w-[50rem] max-w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${className}`}
|
||||
ref={setPopperElement}
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
@ -261,7 +261,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
|
@ -7,7 +7,10 @@ import { Combobox } from "@headlessui/react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useEstimate } from "@/hooks/store";
|
||||
import {
|
||||
useAppRouter,
|
||||
// useEstimate
|
||||
} from "@/hooks/store";
|
||||
import { useDropdown } from "@/hooks/use-dropdown";
|
||||
// components
|
||||
import { DropdownButton } from "./buttons";
|
||||
@ -76,8 +79,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
// store hooks
|
||||
const { workspaceSlug } = useAppRouter();
|
||||
const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate();
|
||||
const activeEstimate = getProjectActiveEstimateDetails(projectId);
|
||||
console.log("workspaceSlug", workspaceSlug);
|
||||
console.log("projectId", projectId);
|
||||
|
||||
// const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate();
|
||||
// const activeEstimate = getProjectActiveEstimateDetails(projectId);
|
||||
const activeEstimate: any = undefined;
|
||||
|
||||
const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({
|
||||
value: point.key,
|
||||
@ -103,10 +110,14 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
|
||||
const selectedEstimate =
|
||||
value !== null
|
||||
? // getEstimatePointValue(value, projectId)
|
||||
null
|
||||
: null;
|
||||
|
||||
const onOpen = async () => {
|
||||
if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId);
|
||||
// if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Ref, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover } from "@headlessui/react";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
// popper
|
||||
// helper
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
@ -42,7 +42,7 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover.Button ref={setReferenceElement} onClick={onClick} disabled={disabled}>
|
||||
<PopoverButton ref={setReferenceElement} onClick={onClick} disabled={disabled}>
|
||||
<div className={`flex items-center gap-2.5 ${getButtonStyling("primary", "lg", disabled)}`}>
|
||||
{icon}
|
||||
<span className="leading-4">{label}</span>
|
||||
@ -55,12 +55,12 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
|
||||
<div className={`absolute bg-blue-400/40 right-0 h-1.5 w-1.5 mt-0.5 mr-0.5 rounded-full`} />
|
||||
</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
{isHovered && (
|
||||
<Popover.Panel
|
||||
<PopoverPanel
|
||||
as="div"
|
||||
className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100 p-5 relative min-w-80"
|
||||
ref={setPopperElement}
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
static
|
||||
@ -68,7 +68,7 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
|
||||
<div className="absolute w-2 h-2 bg-custom-background-100 border rounded-lb-sm border-custom-border-200 border-r-0 border-t-0 transform rotate-45 bottom-2 -left-[5px]" />
|
||||
<h3 className="text-lg font-semibold w-full">{title}</h3>
|
||||
<h4 className="mt-1 text-sm">{description}</h4>
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
|
@ -1,292 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// helpers
|
||||
import { checkDuplicates } from "@/helpers/array.helper";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IEstimate;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
value1: "",
|
||||
value2: "",
|
||||
value3: "",
|
||||
value4: "",
|
||||
value5: "",
|
||||
value6: "",
|
||||
};
|
||||
|
||||
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 hooks
|
||||
const { createEstimate, updateEstimate } = useEstimate();
|
||||
// form info
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
} = useForm<FormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleCreateEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await createEstimate(workspaceSlug.toString(), projectId.toString(), payload)
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_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 handleUpdateEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
await updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload)
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: FormValues) => {
|
||||
if (!formData.name || formData.name === "") {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Estimate title cannot be empty.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
formData.value1 === "" ||
|
||||
formData.value2 === "" ||
|
||||
formData.value3 === "" ||
|
||||
formData.value4 === "" ||
|
||||
formData.value5 === "" ||
|
||||
formData.value6 === ""
|
||||
) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Estimate point cannot be empty.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
formData.value1.length > 20 ||
|
||||
formData.value2.length > 20 ||
|
||||
formData.value3.length > 20 ||
|
||||
formData.value4.length > 20 ||
|
||||
formData.value5.length > 20 ||
|
||||
formData.value6.length > 20
|
||||
) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Estimate point cannot have more than 20 characters.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
checkDuplicates([
|
||||
formData.value1,
|
||||
formData.value2,
|
||||
formData.value3,
|
||||
formData.value4,
|
||||
formData.value5,
|
||||
formData.value6,
|
||||
])
|
||||
) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Estimate points cannot have duplicate values.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: IEstimateFormData = {
|
||||
estimate: {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
},
|
||||
estimate_points: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const point = {
|
||||
key: i,
|
||||
value: formData[`value${i + 1}` as keyof FormValues],
|
||||
};
|
||||
|
||||
if (data)
|
||||
payload.estimate_points.push({
|
||||
id: data.points[i].id,
|
||||
...point,
|
||||
});
|
||||
else payload.estimate_points.push({ ...point });
|
||||
}
|
||||
|
||||
if (data) await handleUpdateEstimate(payload);
|
||||
else await handleCreateEstimate(payload);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data)
|
||||
reset({
|
||||
...defaultValues,
|
||||
...data,
|
||||
value1: data.points[0]?.value,
|
||||
value2: data.points[1]?.value,
|
||||
value3: data.points[2]?.value,
|
||||
value4: data.points[3]?.value,
|
||||
value5: data.points[4]?.value,
|
||||
value6: data.points[5]?.value,
|
||||
});
|
||||
else reset({ ...defaultValues });
|
||||
}, [data, reset]);
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} Estimate</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="name"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
value={value}
|
||||
placeholder="Description"
|
||||
onChange={onChange}
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
{Array(6)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div className="flex items-center" key={i}>
|
||||
<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}
|
||||
rules={{
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: "Estimate point must at most be of 20 characters",
|
||||
},
|
||||
}}
|
||||
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="w-full rounded-l-none"
|
||||
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update Estimate") : isSubmitting ? "Creating" : "Create Estimate"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
data: IEstimate | null;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
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 hooks
|
||||
const { deleteEstimate } = useEstimate();
|
||||
|
||||
const handleEstimateDelete = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
const estimateId = data?.id!;
|
||||
|
||||
await deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be deleted. Please try again",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={onClose}
|
||||
handleSubmit={handleEstimateDelete}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete Estimate"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete estimate-{" "}
|
||||
<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.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
@ -1,114 +0,0 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { IEstimate } from "@plane/types";
|
||||
import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { orderArrayBy } from "@/helpers/array.helper";
|
||||
import { useProject } from "@/hooks/store";
|
||||
// ui
|
||||
//icons
|
||||
// helpers
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
estimate: IEstimate;
|
||||
editEstimate: (estimate: IEstimate) => void;
|
||||
deleteEstimate: (estimateId: string) => void;
|
||||
};
|
||||
|
||||
export const EstimateListItem: React.FC<Props> = observer((props) => {
|
||||
const { estimate, editEstimate, deleteEstimate } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { currentProjectDetails, updateProject } = useProject();
|
||||
|
||||
const handleUseEstimate = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await updateProject(workspaceSlug.toString(), projectId.toString(), {
|
||||
estimate: estimate.id,
|
||||
}).catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate points could not be used. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gap-2 border-b border-custom-border-100 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
||||
{estimate.name}
|
||||
{currentProjectDetails?.estimate && currentProjectDetails?.estimate === estimate.id && (
|
||||
<span className="rounded bg-green-500/20 px-2 py-0.5 text-xs text-green-500">In use</span>
|
||||
)}
|
||||
</h6>
|
||||
<p className="font-sm w-[40vw] truncate text-[14px] font-normal text-custom-text-200">
|
||||
{estimate.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentProjectDetails?.estimate !== estimate?.id && estimate?.points?.length > 0 && (
|
||||
<Button variant="neutral-primary" onClick={handleUseEstimate} size="sm">
|
||||
Use
|
||||
</Button>
|
||||
)}
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
editEstimate(estimate);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
<span>Edit estimate</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{currentProjectDetails?.estimate !== estimate.id && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
deleteEstimate(estimate.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span>Delete estimate</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
{estimate?.points?.length > 0 ? (
|
||||
<div className="flex text-xs text-custom-text-200">
|
||||
Estimate points (
|
||||
<span className="flex gap-1">
|
||||
{orderArrayBy(estimate.points, "key").map((point, index) => (
|
||||
<h6 key={point.id} className="text-custom-text-200">
|
||||
{point.value}
|
||||
{index !== estimate.points.length - 1 && ","}{" "}
|
||||
</h6>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-xs text-custom-text-200">No estimate points</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,121 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { IEstimate } from "@plane/types";
|
||||
// store hooks
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "@/components/estimates-legacy";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { orderArrayBy } from "@/helpers/array.helper";
|
||||
import { useEstimate, useProject } from "@/hooks/store";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
// helpers
|
||||
// constants
|
||||
|
||||
export const EstimatesList: React.FC = observer(() => {
|
||||
// states
|
||||
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
|
||||
const [estimateToDelete, setEstimateToDelete] = useState<string | null>(null);
|
||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { updateProject, currentProjectDetails } = useProject();
|
||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
||||
|
||||
const editEstimate = (estimate: IEstimate) => {
|
||||
setEstimateFormOpen(true);
|
||||
// Order the points array by key before updating the estimate to update state
|
||||
setEstimateToUpdate({
|
||||
...estimate,
|
||||
points: orderArrayBy(estimate.points, "key"),
|
||||
});
|
||||
};
|
||||
|
||||
const disableEstimates = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateProject(workspaceSlug.toString(), projectId.toString(), { estimate: null }).catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_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={getProjectEstimateById(estimateToDelete!)}
|
||||
/>
|
||||
|
||||
<section className="flex items-center justify-between border-b border-custom-border-100 py-3.5">
|
||||
<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);
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
Add Estimate
|
||||
</Button>
|
||||
{currentProjectDetails?.estimate && (
|
||||
<Button variant="neutral-primary" onClick={disableEstimates} size="sm">
|
||||
Disable Estimates
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{projectEstimates ? (
|
||||
projectEstimates.length > 0 ? (
|
||||
<section className="h-full overflow-y-auto bg-custom-background-100">
|
||||
{projectEstimates.map((estimate) => (
|
||||
<EstimateListItem
|
||||
key={estimate.id}
|
||||
estimate={estimate}
|
||||
editEstimate={(estimate) => editEstimate(estimate)}
|
||||
deleteEstimate={(estimateId) => setEstimateToDelete(estimateId)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState type={EmptyStateType.PROJECT_SETTINGS_ESTIMATE} />
|
||||
</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 +0,0 @@
|
||||
export * from "./create-update-estimate-modal";
|
||||
export * from "./delete-estimate-modal";
|
||||
export * from "./estimate-list-item";
|
||||
export * from "./estimates-list";
|
@ -2,32 +2,35 @@ import { FC, useEffect, useMemo, useState } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { IEstimate } from "@plane/types";
|
||||
import { Button } from "@plane/ui";
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/components/estimates";
|
||||
import { TEstimateSystemKeys, EEstimateSystem, TEstimateSystemKeyObject } from "@/components/estimates/types";
|
||||
import { TEstimateSystemKeys, EEstimateSystem, TEstimatePointsObject } from "@/components/estimates/types";
|
||||
// constants
|
||||
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||
// ee components
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
type TCreateEstimateModal = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IEstimate;
|
||||
};
|
||||
|
||||
export const CreateEstimateModal: FC<Props> = observer((props) => {
|
||||
export const CreateEstimateModal: FC<TCreateEstimateModal> = observer((props) => {
|
||||
// props
|
||||
const { handleClose, isOpen } = props;
|
||||
const { workspaceSlug, projectId, isOpen, handleClose } = props;
|
||||
// hooks
|
||||
const { createEstimate } = useProjectEstimates();
|
||||
// states
|
||||
const [estimateSystem, setEstimateSystem] = useState<TEstimateSystemKeys>(EEstimateSystem.POINTS);
|
||||
const [estimatePoints, setEstimatePoints] = useState<TEstimateSystemKeyObject[TEstimateSystemKeys] | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||
|
||||
const handleUpdatePoints = (newPoints: TEstimateSystemKeyObject[TEstimateSystemKeys] | undefined) => {
|
||||
const handleUpdatePoints = (newPoints: TEstimatePointsObject[] | undefined) => {
|
||||
const points = cloneDeep(newPoints);
|
||||
setEstimatePoints(points);
|
||||
};
|
||||
@ -42,6 +45,46 @@ export const CreateEstimateModal: FC<Props> = observer((props) => {
|
||||
// derived values
|
||||
const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]);
|
||||
|
||||
const handleCreateEstimate = async () => {
|
||||
try {
|
||||
const validatedEstimatePoints: TEstimatePointsObject[] = [];
|
||||
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) {
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimatePoint.value && Number(estimatePoint.value)) validatedEstimatePoints.push(estimatePoint);
|
||||
});
|
||||
} else {
|
||||
estimatePoints?.map((estimatePoint) => {
|
||||
if (estimatePoint.value) validatedEstimatePoints.push(estimatePoint);
|
||||
});
|
||||
}
|
||||
|
||||
if (validatedEstimatePoints.length === estimatePoints?.length) {
|
||||
const payload: IEstimateFormData = {
|
||||
estimate: {
|
||||
type: estimateSystem,
|
||||
},
|
||||
estimate_points: validatedEstimatePoints,
|
||||
};
|
||||
|
||||
await createEstimate(workspaceSlug, projectId, payload);
|
||||
handleClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "something went wrong",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "something went wrong",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<div className="relative space-y-6 py-5">
|
||||
@ -70,7 +113,7 @@ export const CreateEstimateModal: FC<Props> = observer((props) => {
|
||||
<EstimateCreateStageOne
|
||||
estimateSystem={estimateSystem}
|
||||
handleEstimateSystem={setEstimateSystem}
|
||||
handleEstimatePoints={(templateType) =>
|
||||
handleEstimatePoints={(templateType: string) =>
|
||||
handleUpdatePoints(ESTIMATE_SYSTEMS[estimateSystem].templates[templateType].values)
|
||||
}
|
||||
/>
|
||||
@ -89,7 +132,7 @@ export const CreateEstimateModal: FC<Props> = observer((props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
{estimatePoints && (
|
||||
<Button variant="primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="primary" size="sm" onClick={handleCreateEstimate}>
|
||||
Create Estimate
|
||||
</Button>
|
||||
)}
|
||||
|
@ -9,7 +9,7 @@ import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||
type TEstimateCreateStageOne = {
|
||||
estimateSystem: TEstimateSystemKeys;
|
||||
handleEstimateSystem: (value: TEstimateSystemKeys) => void;
|
||||
handleEstimatePoints: (value: TEstimateSystemKeys) => void;
|
||||
handleEstimatePoints: (value: string) => void;
|
||||
};
|
||||
|
||||
export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
||||
@ -44,7 +44,7 @@ export const EstimateCreateStageOne: FC<TEstimateCreateStageOne> = (props) => {
|
||||
<button
|
||||
key={name}
|
||||
className="border border-custom-border-200 rounded-md p-2 text-left"
|
||||
onClick={() => handleEstimatePoints(name as TEstimateSystemKeys)}
|
||||
onClick={() => handleEstimatePoints(name)}
|
||||
>
|
||||
<p className="block text-sm">{currentEstimateSystem.templates[name]?.title}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
|
@ -3,20 +3,14 @@ import { Plus } from "lucide-react";
|
||||
import { Button, Sortable } from "@plane/ui";
|
||||
// components
|
||||
import { EstimateItem } from "@/components/estimates";
|
||||
import {
|
||||
EEstimateSystem,
|
||||
TEstimatePointNumeric,
|
||||
TEstimatePointString,
|
||||
TEstimateSystemKeyObject,
|
||||
TEstimateSystemKeys,
|
||||
} from "@/components/estimates/types";
|
||||
import { EEstimateSystem, TEstimatePointsObject } from "@/components/estimates/types";
|
||||
// constants
|
||||
import { ESTIMATE_SYSTEMS } from "@/constants/estimates";
|
||||
|
||||
type TEstimateCreateStageTwo = {
|
||||
estimateSystem: EEstimateSystem;
|
||||
estimatePoints: TEstimateSystemKeyObject[TEstimateSystemKeys];
|
||||
handleEstimatePoints: (value: TEstimateSystemKeyObject[TEstimateSystemKeys]) => void;
|
||||
estimatePoints: TEstimatePointsObject[];
|
||||
handleEstimatePoints: (value: TEstimatePointsObject[]) => void;
|
||||
};
|
||||
|
||||
export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
|
||||
@ -26,20 +20,12 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
|
||||
|
||||
const addNewEstimationPoint = () => {
|
||||
const currentEstimationPoints = estimatePoints;
|
||||
if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) {
|
||||
const newEstimationPoint: TEstimatePointNumeric = {
|
||||
|
||||
const newEstimationPoint: TEstimatePointsObject = {
|
||||
key: currentEstimationPoints.length + 1,
|
||||
value: 0,
|
||||
value: "0",
|
||||
};
|
||||
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint] as TEstimatePointNumeric[]);
|
||||
}
|
||||
if (estimateSystem === EEstimateSystem.CATEGORIES) {
|
||||
const newEstimationPoint: TEstimatePointString = {
|
||||
key: currentEstimationPoints.length + 1,
|
||||
value: "",
|
||||
};
|
||||
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint] as TEstimatePointString[]);
|
||||
}
|
||||
handleEstimatePoints([...currentEstimationPoints, newEstimationPoint]);
|
||||
};
|
||||
|
||||
const deleteEstimationPoint = (index: number) => {
|
||||
@ -48,26 +34,21 @@ export const EstimateCreateStageTwo: FC<TEstimateCreateStageTwo> = (props) => {
|
||||
handleEstimatePoints(newEstimationPoints);
|
||||
};
|
||||
|
||||
const updatedSortedKeys = (updatedEstimatePoints: TEstimateSystemKeyObject[TEstimateSystemKeys]) =>
|
||||
const updatedSortedKeys = (updatedEstimatePoints: TEstimatePointsObject[]) =>
|
||||
updatedEstimatePoints.map((item, index) => ({
|
||||
...item,
|
||||
key: index + 1,
|
||||
})) as TEstimateSystemKeyObject[TEstimateSystemKeys];
|
||||
})) as TEstimatePointsObject[];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Sortable
|
||||
data={estimatePoints as any}
|
||||
render={(value: TEstimatePointString | TEstimatePointNumeric, index: number) => (
|
||||
data={estimatePoints}
|
||||
render={(value: TEstimatePointsObject, index: number) => (
|
||||
<EstimateItem item={value} deleteItem={() => deleteEstimationPoint(index)} />
|
||||
)}
|
||||
onChange={(data: TEstimateSystemKeyObject[TEstimateSystemKeys]) => {
|
||||
console.log("updatedSortedKeys(data)", updatedSortedKeys(data));
|
||||
handleEstimatePoints(updatedSortedKeys(data));
|
||||
}}
|
||||
keyExtractor={(item: TEstimatePointString | TEstimatePointNumeric, index: number) =>
|
||||
item?.id?.toString() || item.value.toString()
|
||||
}
|
||||
onChange={(data: TEstimatePointsObject[]) => handleEstimatePoints(updatedSortedKeys(data))}
|
||||
keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()}
|
||||
/>
|
||||
|
||||
<Button prependIcon={<Plus />} onClick={addNewEstimationPoint}>
|
||||
|
@ -3,10 +3,10 @@ import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react"
|
||||
import { Select } from "@headlessui/react";
|
||||
import { Draggable } from "@plane/ui";
|
||||
import { InlineEdit } from "./inline-editable";
|
||||
import { TEstimatePointNumeric, TEstimatePointString } from "./types";
|
||||
import { TEstimatePointsObject } from "./types";
|
||||
|
||||
type Props = {
|
||||
item: TEstimatePointNumeric | TEstimatePointString;
|
||||
item: TEstimatePointsObject;
|
||||
deleteItem: () => void;
|
||||
};
|
||||
const EstimateItem = ({ item, deleteItem }: Props) => {
|
||||
|
@ -3,6 +3,7 @@ export * from "./loader-screen";
|
||||
|
||||
export * from "./estimate-search";
|
||||
export * from "./estimate-disable";
|
||||
export * from "./inline-editable";
|
||||
|
||||
export * from "./root";
|
||||
export * from "./estimate-item";
|
||||
|
@ -7,7 +7,8 @@ type Props = {
|
||||
inputType?: HTMLInputTypeAttribute;
|
||||
isEditing?: boolean;
|
||||
};
|
||||
const InlineEdit = ({
|
||||
|
||||
export const InlineEdit = ({
|
||||
onSave,
|
||||
value: defaultValue,
|
||||
inputType = "text",
|
||||
@ -62,5 +63,3 @@ const InlineEdit = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { InlineEdit };
|
||||
|
@ -5,7 +5,6 @@ import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { EstimateEmptyScreen, EstimateLoaderScreen, CreateEstimateModal } from "@/components/estimates";
|
||||
import { DeleteEstimateModal, EstimateListItem } from "@/components/estimates-legacy";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// ee components
|
||||
@ -21,29 +20,26 @@ export const EstimateRoot: FC<TEstimateRoot> = (props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// hooks
|
||||
const { updateProject, currentProjectDetails } = useProject();
|
||||
const { loader, projectEstimateIds, estimateById, getAllEstimates } = useProjectEstimates(projectId);
|
||||
const { loader, projectEstimateIds, estimateById, getProjectEstimates } = useProjectEstimates();
|
||||
// states
|
||||
const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(false);
|
||||
const [isEstimateDeleteModalOpen, setIsEstimateDeleteModalOpen] = useState<string | null>(null);
|
||||
// const [isEstimateDeleteModalOpen, setIsEstimateDeleteModalOpen] = useState<string | null>(null);
|
||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
||||
|
||||
console.log("workspaceSlug", workspaceSlug);
|
||||
console.log("projectId", projectId);
|
||||
|
||||
const { isLoading: isSWRLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
||||
async () => workspaceSlug && projectId && getAllEstimates()
|
||||
async () => workspaceSlug && projectId && getProjectEstimates(workspaceSlug, projectId)
|
||||
);
|
||||
|
||||
const editEstimate = (estimate: IEstimate) => {
|
||||
setIsEstimateCreateModalOpen(true);
|
||||
console.log("estimate", estimate);
|
||||
// Order the points array by key before updating the estimate to update state
|
||||
// setEstimateToUpdate({
|
||||
// ...estimate,
|
||||
// points: orderArrayBy(estimate.points, "key"),
|
||||
// });
|
||||
};
|
||||
// const editEstimate = (estimate: IEstimate) => {
|
||||
// setIsEstimateCreateModalOpen(true);
|
||||
// console.log("estimate", estimate);
|
||||
// // Order the points array by key before updating the estimate to update state
|
||||
// // setEstimateToUpdate({
|
||||
// // ...estimate,
|
||||
// // points: orderArrayBy(estimate.points, "key"),
|
||||
// // });
|
||||
// };
|
||||
|
||||
const disableEstimates = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
@ -64,6 +60,7 @@ export const EstimateRoot: FC<TEstimateRoot> = (props) => {
|
||||
<div className="container mx-auto">
|
||||
<EstimateLoaderScreen />
|
||||
<EstimateEmptyScreen onButtonClick={() => {}} />
|
||||
|
||||
{loader === "init-loader" || isSWRLoading ? (
|
||||
<Loader className="mt-5 space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
@ -109,12 +106,13 @@ export const EstimateRoot: FC<TEstimateRoot> = (props) => {
|
||||
const estimate = estimateById(estimateId);
|
||||
if (!estimate) return <></>;
|
||||
return (
|
||||
<EstimateListItem
|
||||
key={estimateId}
|
||||
estimate={estimate}
|
||||
editEstimate={(estimate) => editEstimate(estimate)}
|
||||
deleteEstimate={(estimateId) => setIsEstimateDeleteModalOpen(estimateId)}
|
||||
/>
|
||||
<></>
|
||||
// <EstimateListItem
|
||||
// key={estimateId}
|
||||
// estimate={estimate}
|
||||
// editEstimate={(estimate) => editEstimate(estimate)}
|
||||
// deleteEstimate={(estimateId) => setIsEstimateDeleteModalOpen(estimateId)}
|
||||
// />
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
@ -122,7 +120,10 @@ export const EstimateRoot: FC<TEstimateRoot> = (props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* */}
|
||||
<CreateEstimateModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={isEstimateCreateModalOpen}
|
||||
data={estimateToUpdate}
|
||||
handleClose={() => {
|
||||
@ -130,14 +131,12 @@ export const EstimateRoot: FC<TEstimateRoot> = (props) => {
|
||||
setEstimateToUpdate(undefined);
|
||||
}}
|
||||
/>
|
||||
<DeleteEstimateModal
|
||||
|
||||
{/* <DeleteEstimateModal
|
||||
isOpen={!!isEstimateDeleteModalOpen}
|
||||
handleClose={() => setIsEstimateDeleteModalOpen(null)}
|
||||
data={
|
||||
null
|
||||
// getProjectEstimateById(isEstimateDeleteModalOpen!)
|
||||
}
|
||||
/>
|
||||
data={}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -5,25 +5,19 @@ export enum EEstimateSystem {
|
||||
}
|
||||
export type TEstimateSystemKeys = EEstimateSystem.POINTS | EEstimateSystem.CATEGORIES | EEstimateSystem.TIME;
|
||||
|
||||
export type TEstimatePointString = { key: number; value: string; id: string };
|
||||
export type TEstimatePointNumeric = { key: number; value: number; id: string };
|
||||
export type TEstimateSystemKeyObject = {
|
||||
points: TEstimatePointNumeric[];
|
||||
categories: TEstimatePointString[];
|
||||
time: TEstimatePointNumeric[];
|
||||
};
|
||||
export type TEstimatePointsObject = { id?: string | undefined; key: number; value: string };
|
||||
|
||||
export type TTemplateValues<T extends TEstimateSystemKeys> = {
|
||||
export type TTemplateValues = {
|
||||
title: string;
|
||||
values: TEstimateSystemKeyObject[T];
|
||||
values: TEstimatePointsObject[];
|
||||
};
|
||||
|
||||
export type TEstimateSystem<T extends TEstimateSystemKeys> = {
|
||||
export type TEstimateSystem = {
|
||||
name: string;
|
||||
templates: Record<string, TTemplateValues<T>>;
|
||||
templates: Record<string, TTemplateValues>;
|
||||
is_available: boolean;
|
||||
};
|
||||
|
||||
export type TEstimateSystems = {
|
||||
[K in TEstimateSystemKeys]: TEstimateSystem<K>;
|
||||
[K in TEstimateSystemKeys]: TEstimateSystem;
|
||||
};
|
||||
|
@ -1,207 +0,0 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { ChevronLeft, Plus } from "lucide-react";
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// ui
|
||||
import { SubHeading, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { RadioInput } from "@/components/radio-group";
|
||||
import { Sortable } from "@/components/sortable/sortable";
|
||||
import { EstimateItem } from "./estimate-item";
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
import { useRouter } from "next/router";
|
||||
// helpers
|
||||
// hooks
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IEstimate;
|
||||
};
|
||||
|
||||
const ESTIMATE_SYSTEMS = {
|
||||
Points: {
|
||||
name: "Points",
|
||||
templates: {
|
||||
Fibonacci: [1, 2, 3, 5, 8, 13, 21],
|
||||
Linear: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
Squares: [1, 4, 9, 16, 25, 36],
|
||||
},
|
||||
},
|
||||
Categories: {
|
||||
name: "Categories",
|
||||
templates: {
|
||||
"T-Shirt Sizes": ["XS", "S", "M", "L", "XL", "XXL"],
|
||||
"Easy to hard": ["Easy", "Medium", "Hard", "Very Hard"],
|
||||
},
|
||||
},
|
||||
Time: {
|
||||
name: "Time",
|
||||
templates: { Hours: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] },
|
||||
},
|
||||
};
|
||||
|
||||
type EstimatePoint = {
|
||||
id?: string;
|
||||
key: number;
|
||||
value: string;
|
||||
};
|
||||
export const UpdateEstimateModal: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, isOpen, data } = props;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { createEstimate, updateEstimate } = useEstimate();
|
||||
|
||||
console.log({ data });
|
||||
|
||||
const [estimateSystem, setEstimateSystem] = useState("Points");
|
||||
const [points, setPoints] = useState<EstimatePoint[] | null>(data.points);
|
||||
|
||||
const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem];
|
||||
|
||||
const deleteItem = (index: number) => {
|
||||
points.splice(index, 1);
|
||||
setPoints([...points]);
|
||||
};
|
||||
|
||||
const saveEstimate = async () => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
console.log({ points });
|
||||
|
||||
const payload: IEstimateFormData = {
|
||||
estimate_points: points?.map((point, index) => {
|
||||
point.key = index;
|
||||
return point;
|
||||
}),
|
||||
estimate: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
},
|
||||
};
|
||||
|
||||
console.log({ payload });
|
||||
|
||||
await updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload)
|
||||
.then(() => {
|
||||
// onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<div className="p-5">
|
||||
{!points && (
|
||||
<Fragment>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex justify-start align-middle items-center ">
|
||||
<SubHeading noMargin>New Estimate System</SubHeading>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-gray-400">Step 2/2</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={() => console.log("Submitted")}>
|
||||
<div className="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0 gap-2 mb-2">
|
||||
<RadioInput
|
||||
options={Object.keys(ESTIMATE_SYSTEMS).map((name) => ({ label: name, value: name }))}
|
||||
label="Choose an estimate system"
|
||||
selected={estimateSystem}
|
||||
onChange={(value) => setEstimateSystem(value)}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<SubHeading>Choose a template</SubHeading>
|
||||
<div className="flex flex-wrap gap-5 grid sm:grid-cols-2 mb-8">
|
||||
{Object.keys(currentEstimateSystem.templates).map((name) => (
|
||||
<button
|
||||
key={name}
|
||||
className=" border border-custom-border-200 rounded-md p-2 text-left"
|
||||
onClick={() =>
|
||||
setPoints(
|
||||
currentEstimateSystem.templates[name].map((value: number | string, key: number) => ({
|
||||
value,
|
||||
key,
|
||||
}))
|
||||
)
|
||||
}
|
||||
>
|
||||
<p className="block text-sm">{name}</p>
|
||||
<p className="text-xs text-gray-400">{currentEstimateSystem.templates[name].join(", ")}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add modal footer */}
|
||||
</form>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{points && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex justify-start items-center">
|
||||
<button onClick={() => setPoints(null)}>
|
||||
<ChevronLeft className="w-6 h-6 mr-1" />
|
||||
</button>
|
||||
<SubHeading noMargin>New Estimate System</SubHeading>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-gray-400">Step 2/2</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sortable
|
||||
data={points}
|
||||
render={(value: string, index: number) => (
|
||||
<EstimateItem item={value} deleteItem={() => deleteItem(index)} />
|
||||
)}
|
||||
onChange={(data: number[]) => setPoints(data)}
|
||||
keyExtractor={(value: number) => value}
|
||||
/>
|
||||
<button
|
||||
className=" bg-custom-primary text-white rounded-md px-3 py-1 flex items-center gap-1"
|
||||
onClick={() => {
|
||||
setPoints([...points, ""]);
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span className="text-sm">Add {estimateSystem}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-5 border-t -m-5 px-5 py-3 ">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="inline-flex justify-center px-4 py-1 text-sm font-medium text-white bg-custom-primary border border-transparent rounded-md shadow-sm hover:bg-custom-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-primary-dark"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{points && (
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center px-4 py-1 ml-3 text-sm font-medium text-white bg-custom-primary border border-transparent rounded-md shadow-sm hover:bg-custom-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-primary-dark"
|
||||
onClick={saveEstimate}
|
||||
>
|
||||
Create Estimate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
@ -5,7 +5,7 @@ import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// types
|
||||
import { TEstimateSystemKeys, TEstimateSystemKeyObject } from "@/components/estimates/types";
|
||||
import { TEstimatePointsObject } from "@/components/estimates/types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -17,9 +17,7 @@ export const UpdateEstimateModal: FC<Props> = observer((props) => {
|
||||
// props
|
||||
const { handleClose, isOpen } = props;
|
||||
// states
|
||||
const [estimatePoints, setEstimatePoints] = useState<TEstimateSystemKeyObject[TEstimateSystemKeys] | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [estimatePoints, setEstimatePoints] = useState<TEstimatePointsObject[] | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
|
@ -17,7 +17,7 @@ import { IssueLabelSelect } from "@/components/issues/select";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
import { useProjectEstimates } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueProperties = {
|
||||
projectId: string;
|
||||
@ -29,7 +29,7 @@ type TInboxIssueProperties = {
|
||||
export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props) => {
|
||||
const { projectId, data, handleData, isVisible = false } = props;
|
||||
// hooks
|
||||
const { areEstimatesEnabledForProject } = useEstimate();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
// states
|
||||
const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | undefined>(undefined);
|
||||
@ -142,7 +142,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
|
||||
)}
|
||||
|
||||
{/* estimate */}
|
||||
{isVisible && areEstimatesEnabledForProject(projectId) && (
|
||||
{isVisible && areEstimateEnabledByProjectId(projectId) && (
|
||||
<div className="h-7">
|
||||
<EstimateDropdown
|
||||
value={data?.estimate_point || null}
|
||||
|
@ -2,7 +2,10 @@ import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Triangle } from "lucide-react";
|
||||
// hooks
|
||||
import { useEstimate, useIssueDetail } from "@/hooks/store";
|
||||
import {
|
||||
// useEstimate,
|
||||
useIssueDetail,
|
||||
} from "@/hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
@ -14,13 +17,15 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
// const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
const areEstimatesEnabledForCurrentProject = false;
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
|
||||
const estimateValue = getEstimatePointValue(Number(activity.new_value), null);
|
||||
// const estimateValue = getEstimatePointValue(Number(activity.new_value), null);
|
||||
const estimateValue = "None";
|
||||
const currentPoint = Number(activity.new_value) + 1;
|
||||
|
||||
return (
|
||||
|
@ -54,7 +54,13 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// types
|
||||
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
|
||||
import {
|
||||
// useEstimate,
|
||||
useIssueDetail,
|
||||
useProject,
|
||||
useProjectState,
|
||||
useUser,
|
||||
} from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import type { TIssueOperations } from "./root";
|
||||
@ -82,7 +88,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { data: currentUser } = useUser();
|
||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
// const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
const areEstimatesEnabledForCurrentProject = false;
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
@ -26,7 +26,7 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState, useProject } from "@/hooks/store";
|
||||
import { useEventTracker, useLabel, useIssues, useProjectState, useProject, useProjectEstimates } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local components
|
||||
import { IssuePropertyLabels } from "../properties/labels";
|
||||
@ -42,6 +42,9 @@ export interface IIssueProperties {
|
||||
}
|
||||
|
||||
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
@ -53,13 +56,10 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const {
|
||||
issues: { addCycleToIssue, removeCycleFromIssue },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
const { getStateById } = useProjectState();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const currentLayout = `${activeLayout} layout`;
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
@ -394,7 +394,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{areEstimatesEnabledForCurrentProject && (
|
||||
{projectId && areEstimateEnabledByProjectId(projectId?.toString()) && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<EstimateDropdown
|
||||
|
@ -28,7 +28,14 @@ import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"
|
||||
import { getChangedIssuefields, getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||
import { shouldRenderProject } from "@/helpers/project.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useEstimate, useInstance, useIssueDetail, useProject, useWorkspace } from "@/hooks/store";
|
||||
import {
|
||||
useAppRouter,
|
||||
useInstance,
|
||||
useIssueDetail,
|
||||
useProject,
|
||||
useWorkspace,
|
||||
useProjectEstimates,
|
||||
} from "@/hooks/store";
|
||||
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
@ -118,7 +125,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const { projectId: routeProjectId } = useAppRouter();
|
||||
const { config } = useInstance();
|
||||
const { getProjectById } = useProject();
|
||||
const { areEstimatesEnabledForProject } = useEstimate();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
@ -631,7 +638,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{areEstimatesEnabledForProject(projectId) && (
|
||||
{projectId && areEstimateEnabledByProjectId(projectId) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, Ref, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@ -6,7 +6,7 @@ import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// plane ui
|
||||
@ -104,7 +104,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
<Menu as="div" className="relative h-full flex-grow truncate text-left">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
|
||||
<MenuButton className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
|
||||
<div
|
||||
className={`flex items-center gap-x-2 truncate rounded p-1 ${
|
||||
sidebarCollapsed ? "justify-center" : "justify-between"
|
||||
@ -126,7 +126,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
@ -136,7 +136,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items as={Fragment}>
|
||||
<MenuItems as={Fragment}>
|
||||
<div className="fixed left-4 z-20 mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
|
||||
<div className="vertical-scrollbar scrollbar-sm mb-2 flex max-h-96 flex-col items-start justify-start gap-2 overflow-y-scroll px-4">
|
||||
<h6 className="sticky top-0 z-10 h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-sidebar-text-400">
|
||||
@ -155,7 +155,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Menu.Item
|
||||
<MenuItem
|
||||
as="div"
|
||||
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
@ -188,7 +188,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
|
||||
</span>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@ -203,13 +203,13 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
|
||||
<Link href="/create-workspace" className="w-full">
|
||||
<Menu.Item
|
||||
<MenuItem
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" />
|
||||
Create workspace
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
||||
<Link
|
||||
@ -220,18 +220,18 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
if (index > 0) handleItemClick();
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
<MenuItem
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<link.icon className="h-4 w-4 flex-shrink-0" />
|
||||
{link.name}
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full px-4 py-2">
|
||||
<Menu.Item
|
||||
<MenuItem
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
|
||||
@ -239,17 +239,17 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
>
|
||||
<LogOut className="h-4 w-4 flex-shrink-0" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
|
||||
<MenuButton className="grid place-items-center outline-none" ref={setReferenceElement}>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={currentUser?.avatar || undefined}
|
||||
@ -257,7 +257,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
@ -267,10 +267,10 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
<MenuItems
|
||||
className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y
|
||||
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
|
||||
ref={setPopperElement}
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
@ -284,17 +284,17 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
if (index == 0) handleItemClick();
|
||||
}}
|
||||
>
|
||||
<Menu.Item key={index} as="div">
|
||||
<MenuItem key={index} as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<link.icon className="h-4 w-4 stroke-[1.5]" />
|
||||
{link.name}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
|
||||
<Menu.Item
|
||||
<MenuItem
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
@ -302,20 +302,20 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
<div className="p-2 pb-0">
|
||||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<MenuItem as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
Enter God Mode
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
|
@ -7,39 +7,39 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
||||
fibonacci: {
|
||||
title: "Fibonacci",
|
||||
values: [
|
||||
{ key: 1, value: 1 },
|
||||
{ key: 2, value: 2 },
|
||||
{ key: 3, value: 3 },
|
||||
{ key: 4, value: 5 },
|
||||
{ key: 5, value: 8 },
|
||||
{ key: 6, value: 13 },
|
||||
{ key: 7, value: 21 },
|
||||
{ id: undefined, key: 1, value: "1" },
|
||||
{ id: undefined, key: 2, value: "2" },
|
||||
{ id: undefined, key: 3, value: "3" },
|
||||
{ id: undefined, key: 4, value: "5" },
|
||||
{ id: undefined, key: 5, value: "8" },
|
||||
{ id: undefined, key: 6, value: "13" },
|
||||
{ id: undefined, key: 7, value: "21" },
|
||||
],
|
||||
},
|
||||
linear: {
|
||||
title: "Linear",
|
||||
values: [
|
||||
{ key: 1, value: 1 },
|
||||
{ key: 2, value: 2 },
|
||||
{ key: 3, value: 3 },
|
||||
{ key: 4, value: 4 },
|
||||
{ key: 5, value: 5 },
|
||||
{ key: 6, value: 6 },
|
||||
{ key: 7, value: 7 },
|
||||
{ key: 8, value: 8 },
|
||||
{ key: 9, value: 9 },
|
||||
{ key: 10, value: 10 },
|
||||
{ id: undefined, key: 1, value: "1" },
|
||||
{ id: undefined, key: 2, value: "2" },
|
||||
{ id: undefined, key: 3, value: "3" },
|
||||
{ id: undefined, key: 4, value: "4" },
|
||||
{ id: undefined, key: 5, value: "5" },
|
||||
{ id: undefined, key: 6, value: "6" },
|
||||
{ id: undefined, key: 7, value: "7" },
|
||||
{ id: undefined, key: 8, value: "8" },
|
||||
{ id: undefined, key: 9, value: "9" },
|
||||
{ id: undefined, key: 10, value: "10" },
|
||||
],
|
||||
},
|
||||
squares: {
|
||||
title: "Squares",
|
||||
values: [
|
||||
{ key: 1, value: 1 },
|
||||
{ key: 2, value: 4 },
|
||||
{ key: 3, value: 9 },
|
||||
{ key: 4, value: 16 },
|
||||
{ key: 5, value: 25 },
|
||||
{ key: 6, value: 36 },
|
||||
{ id: undefined, key: 1, value: "1" },
|
||||
{ id: undefined, key: 2, value: "4" },
|
||||
{ id: undefined, key: 3, value: "9" },
|
||||
{ id: undefined, key: 4, value: "16" },
|
||||
{ id: undefined, key: 5, value: "25" },
|
||||
{ id: undefined, key: 6, value: "36" },
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -51,21 +51,21 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
||||
t_shirt_sizes: {
|
||||
title: "T-Shirt Sizes",
|
||||
values: [
|
||||
{ key: 1, value: "XS" },
|
||||
{ key: 2, value: "S" },
|
||||
{ key: 3, value: "M" },
|
||||
{ key: 4, value: "L" },
|
||||
{ key: 5, value: "XL" },
|
||||
{ key: 6, value: "XXL" },
|
||||
{ id: undefined, key: 1, value: "XS" },
|
||||
{ id: undefined, key: 2, value: "S" },
|
||||
{ id: undefined, key: 3, value: "M" },
|
||||
{ id: undefined, key: 4, value: "L" },
|
||||
{ id: undefined, key: 5, value: "XL" },
|
||||
{ id: undefined, key: 6, value: "XXL" },
|
||||
],
|
||||
},
|
||||
easy_to_hard: {
|
||||
title: "Easy to hard",
|
||||
values: [
|
||||
{ key: 1, value: "Easy" },
|
||||
{ key: 2, value: "Medium" },
|
||||
{ key: 3, value: "Hard" },
|
||||
{ key: 4, value: "Very Hard" },
|
||||
{ id: undefined, key: 1, value: "Easy" },
|
||||
{ id: undefined, key: 2, value: "Medium" },
|
||||
{ id: undefined, key: 3, value: "Hard" },
|
||||
{ id: undefined, key: 4, value: "Very Hard" },
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -77,16 +77,16 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
|
||||
hours: {
|
||||
title: "Hours",
|
||||
values: [
|
||||
{ key: 1, value: 1 },
|
||||
{ key: 2, value: 2 },
|
||||
{ key: 3, value: 3 },
|
||||
{ key: 4, value: 4 },
|
||||
{ key: 5, value: 5 },
|
||||
{ key: 6, value: 6 },
|
||||
{ key: 7, value: 7 },
|
||||
{ key: 8, value: 8 },
|
||||
{ key: 9, value: 9 },
|
||||
{ key: 10, value: 10 },
|
||||
{ id: undefined, key: 1, value: "1" },
|
||||
{ id: undefined, key: 2, value: "2" },
|
||||
{ id: undefined, key: 3, value: "3" },
|
||||
{ id: undefined, key: 4, value: "4" },
|
||||
{ id: undefined, key: 5, value: "5" },
|
||||
{ id: undefined, key: 6, value: "6" },
|
||||
{ id: undefined, key: 7, value: "7" },
|
||||
{ id: undefined, key: 8, value: "8" },
|
||||
{ id: undefined, key: 9, value: "9" },
|
||||
{ id: undefined, key: 10, value: "10" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -4,11 +4,9 @@ import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import { IProjectEstimateStore } from "@/store/estimates/project-estimate.store";
|
||||
|
||||
export const useProjectEstimates = (projectId: string | undefined): IProjectEstimateStore => {
|
||||
export const useProjectEstimates = (): IProjectEstimateStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectPage must be used within StoreProvider");
|
||||
|
||||
if (!projectId) throw new Error("projectId must be passed as a property");
|
||||
|
||||
return context.projectEstimate;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCycle, useEstimate, useLabel, useMember, useModule, useProjectState } from "./store";
|
||||
import { useCycle, useProjectEstimates, useLabel, useMember, useModule, useProjectState } from "./store";
|
||||
|
||||
export const useProjectIssueProperties = () => {
|
||||
const { fetchProjectStates } = useProjectState();
|
||||
@ -8,7 +8,7 @@ export const useProjectIssueProperties = () => {
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { fetchAllCycles: fetchProjectAllCycles } = useCycle();
|
||||
const { fetchModules: fetchProjectAllModules } = useModule();
|
||||
const { fetchProjectEstimates } = useEstimate();
|
||||
const { getProjectEstimates } = useProjectEstimates();
|
||||
|
||||
// fetching project states
|
||||
const fetchStates = async (
|
||||
@ -62,7 +62,7 @@ export const useProjectIssueProperties = () => {
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectEstimates(workspaceSlug.toString(), projectId.toString());
|
||||
await getProjectEstimates(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import useSWR from "swr";
|
||||
import { useCycle, useEstimate, useLabel, useModule, useProjectState } from "./store";
|
||||
import { useCycle, useProjectEstimates, useLabel, useModule, useProjectState } from "./store";
|
||||
|
||||
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
|
||||
const { fetchWorkspaceLabels } = useLabel();
|
||||
|
||||
const { fetchWorkspaceStates } = useProjectState();
|
||||
|
||||
const { fetchWorkspaceEstimates } = useEstimate();
|
||||
const { getWorkspaceEstimates } = useProjectEstimates();
|
||||
|
||||
const { fetchWorkspaceModules } = useModule();
|
||||
|
||||
@ -43,7 +43,7 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
|
||||
// fetch workspace estimates
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => getWorkspaceEstimates(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { EmptyState, LogoSpinner } from "@/components/common";
|
||||
import {
|
||||
useEventTracker,
|
||||
useCycle,
|
||||
useEstimate,
|
||||
useProjectEstimates,
|
||||
useLabel,
|
||||
useMember,
|
||||
useModule,
|
||||
@ -44,7 +44,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
} = useMember();
|
||||
const { fetchProjectStates } = useProjectState();
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { fetchProjectEstimates } = useEstimate();
|
||||
const { getProjectEstimates } = useProjectEstimates();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -80,7 +80,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
// fetching project estimates
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
workspaceSlug && projectId ? () => getProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project cycles
|
||||
|
@ -43,8 +43,6 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Need to handle custom themes for toast */}
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
<InstanceWrapper>
|
||||
<StoreWrapper>
|
||||
<CrispWrapper user={currentUser}>
|
||||
@ -56,7 +54,11 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
posthogAPIKey={config?.posthog_api_key || undefined}
|
||||
posthogHost={config?.posthog_host || undefined}
|
||||
>
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
<SWRConfig value={SWR_CONFIG}>
|
||||
{/* TODO: Need to handle custom themes for toast */}
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
{children}
|
||||
</SWRConfig>
|
||||
</PostHogProvider>
|
||||
</CrispWrapper>
|
||||
</StoreWrapper>
|
||||
|
@ -35,24 +35,24 @@ const nextConfig = {
|
||||
{
|
||||
source: "/accounts/sign-up",
|
||||
destination: "/sign-up",
|
||||
permanent: true
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/sign-in",
|
||||
destination: "/",
|
||||
permanent: true
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/register",
|
||||
destination: "/sign-up",
|
||||
permanent: true
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/login",
|
||||
destination: "/",
|
||||
permanent: true
|
||||
}
|
||||
]
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
const rewrites = [
|
||||
@ -66,13 +66,13 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
if (process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.NEXT_PUBLIC_ADMIN_BASE_PATH) {
|
||||
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""
|
||||
const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""
|
||||
const GOD_MODE_BASE_URL = ADMIN_BASE_URL + ADMIN_BASE_PATH
|
||||
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
|
||||
const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
|
||||
const GOD_MODE_BASE_URL = ADMIN_BASE_URL + ADMIN_BASE_PATH;
|
||||
rewrites.push({
|
||||
source: "/god-mode/:path*",
|
||||
destination: `${GOD_MODE_BASE_URL}/:path*`,
|
||||
})
|
||||
});
|
||||
}
|
||||
return rewrites;
|
||||
},
|
||||
|
@ -25,12 +25,12 @@
|
||||
"@nivo/line": "0.80.0",
|
||||
"@nivo/pie": "0.80.0",
|
||||
"@nivo/scatterplot": "0.80.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/document-editor": "*",
|
||||
"@plane/lite-text-editor": "*",
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/nextjs": "^7.108.0",
|
||||
"axios": "^1.1.3",
|
||||
@ -70,17 +70,17 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "18.0.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
"eslint-config-custom": "*",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
@ -11,7 +11,7 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
|
||||
// fetching the estimates in workspace level
|
||||
async fetchWorkspacesList(workspaceSlug: string): Promise<IEstimate[] | undefined> {
|
||||
async fetchWorkspaceEstimates(workspaceSlug: string): Promise<IEstimate[] | undefined> {
|
||||
try {
|
||||
const { data } = await this.get(`/api/workspaces/${workspaceSlug}/estimates/`);
|
||||
return data || undefined;
|
||||
@ -20,7 +20,7 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAll(workspaceSlug: string, projectId: string): Promise<IEstimate[] | undefined> {
|
||||
async fetchProjectEstimates(workspaceSlug: string, projectId: string): Promise<IEstimate[] | undefined> {
|
||||
try {
|
||||
const { data } = await this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`);
|
||||
return data || undefined;
|
||||
@ -29,7 +29,11 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchById(workspaceSlug: string, projectId: string, estimateId: string): Promise<IEstimate | undefined> {
|
||||
async fetchEstimateById(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string
|
||||
): Promise<IEstimate | undefined> {
|
||||
try {
|
||||
const { data } = await this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
|
||||
@ -40,7 +44,11 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, projectId: string, payload: Partial<IEstimate>): Promise<IEstimate | undefined> {
|
||||
async createEstimate(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
payload: IEstimateFormData
|
||||
): Promise<IEstimate | undefined> {
|
||||
try {
|
||||
const { data } = await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, payload);
|
||||
return data || undefined;
|
||||
@ -49,11 +57,11 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
async updateEstimate(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string,
|
||||
payload: Partial<IEstimate>
|
||||
payload: IEstimateFormData
|
||||
): Promise<IEstimate | undefined> {
|
||||
try {
|
||||
const { data } = await this.patch(
|
||||
@ -66,14 +74,20 @@ export class EstimateService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async remove(workspaceSlug: string, projectId: string, estimateId: string): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
|
||||
);
|
||||
return data || undefined;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
async removeEstimatePoint(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string,
|
||||
estimatePointId: string,
|
||||
payload: { new_estimate_id: string | undefined }
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-point/${estimatePointId}/`,
|
||||
payload
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,222 +0,0 @@
|
||||
import set from "lodash/set";
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// services
|
||||
import { ProjectEstimateService } from "@/services/project";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IEstimateStore {
|
||||
//Loaders
|
||||
fetchedMap: Record<string, boolean>;
|
||||
// observables
|
||||
estimateMap: Record<string, IEstimate>;
|
||||
// computed
|
||||
areEstimatesEnabledForCurrentProject: boolean;
|
||||
projectEstimates: IEstimate[] | null;
|
||||
activeEstimateDetails: IEstimate | null;
|
||||
// computed actions
|
||||
areEstimatesEnabledForProject: (projectId: string) => boolean;
|
||||
getEstimatePointValue: (estimateKey: number | null, projectId: string | null) => string;
|
||||
getProjectEstimateById: (estimateId: string) => IEstimate | null;
|
||||
getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null;
|
||||
// fetch actions
|
||||
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<IEstimate[]>;
|
||||
fetchWorkspaceEstimates: (workspaceSlug: string) => Promise<IEstimate[]>;
|
||||
// crud actions
|
||||
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 EstimateStore implements IEstimateStore {
|
||||
// observables
|
||||
estimateMap: Record<string, IEstimate> = {};
|
||||
//loaders
|
||||
fetchedMap: Record<string, boolean> = {};
|
||||
// root store
|
||||
rootStore;
|
||||
// services
|
||||
estimateService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
estimateMap: observable,
|
||||
fetchedMap: observable,
|
||||
// computed
|
||||
areEstimatesEnabledForCurrentProject: computed,
|
||||
projectEstimates: computed,
|
||||
activeEstimateDetails: computed,
|
||||
// actions
|
||||
fetchProjectEstimates: action,
|
||||
fetchWorkspaceEstimates: 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.router.projectId;
|
||||
const worksapceSlug = this.rootStore.router.workspaceSlug || "";
|
||||
if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return null;
|
||||
return Object.values(this.estimateMap).filter((estimate) => estimate.project === projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.estimateMap?.[currentProjectDetails?.estimate || ""] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if estimates are enabled for a project using project id
|
||||
* @param projectId
|
||||
*/
|
||||
areEstimatesEnabledForProject = computedFn((projectId: string) => {
|
||||
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
|
||||
if (!projectDetails) return false;
|
||||
return Boolean(projectDetails.estimate) ?? false;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns the point value for the given estimate key to display in the UI
|
||||
*/
|
||||
getEstimatePointValue = computedFn((estimateKey: number | null, projectId: string | null) => {
|
||||
if (estimateKey === null) return "None";
|
||||
const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails;
|
||||
return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None";
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns the estimate details for the given estimate id
|
||||
* @param estimateId
|
||||
*/
|
||||
getProjectEstimateById = computedFn((estimateId: string) => {
|
||||
if (!this.projectEstimates) return null;
|
||||
const estimateInfo = this.estimateMap?.[estimateId] || null;
|
||||
return estimateInfo;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns the estimate details for the given estimate id
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectActiveEstimateDetails = computedFn((projectId: string) => {
|
||||
const projectDetails = this.rootStore.projectRoot.project?.getProjectById(projectId);
|
||||
const worksapceSlug = this.rootStore.router.workspaceSlug || "";
|
||||
if (!projectDetails || !projectDetails?.estimate || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug]))
|
||||
return null;
|
||||
return this.estimateMap?.[projectDetails?.estimate || ""] || null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetches the list of estimates for the given project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchProjectEstimates = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.estimateService.getEstimatesList(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((estimate) => {
|
||||
set(this.estimateMap, estimate.id, estimate);
|
||||
});
|
||||
this.fetchedMap[projectId] = true;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetches the list of estimates for the given project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchWorkspaceEstimates = async (workspaceSlug: string) =>
|
||||
await this.estimateService.getWorkspaceEstimatesList(workspaceSlug).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((estimate) => {
|
||||
set(this.estimateMap, estimate.id, estimate);
|
||||
});
|
||||
this.fetchedMap[workspaceSlug] = true;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description creates a new estimate for the given project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
*/
|
||||
createEstimate = async (workspaceSlug: string, projectId: string, data: IEstimateFormData) =>
|
||||
await this.estimateService.createEstimate(workspaceSlug, projectId, data).then((response) => {
|
||||
const responseEstimate = {
|
||||
...response.estimate,
|
||||
points: response.estimate_points,
|
||||
};
|
||||
runInAction(() => {
|
||||
set(this.estimateMap, [responseEstimate.id], responseEstimate);
|
||||
});
|
||||
return response.estimate;
|
||||
});
|
||||
|
||||
/**
|
||||
* @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) =>
|
||||
await this.estimateService.patchEstimate(workspaceSlug, projectId, estimateId, data).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.estimateMap, estimateId, {
|
||||
...this.estimateMap[estimateId],
|
||||
...data.estimate,
|
||||
points: [...data.estimate_points],
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description deletes the given estimate for the given project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param estimateId
|
||||
*/
|
||||
deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) =>
|
||||
await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId).then(() => {
|
||||
runInAction(() => {
|
||||
delete this.estimateMap[estimateId];
|
||||
});
|
||||
});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { IEstimatePoint as IEstimatePointType } from "@plane/types";
|
||||
import { IEstimateFormData, IEstimate, IEstimatePoint as IEstimatePointType } from "@plane/types";
|
||||
// services
|
||||
import { EstimateService } from "@/services/project/estimate.service";
|
||||
// store
|
||||
@ -16,8 +16,7 @@ export interface IEstimatePoint extends IEstimatePointType {
|
||||
// computed
|
||||
asJson: IEstimatePointType;
|
||||
// actions
|
||||
updateEstimatePoint: () => Promise<void>;
|
||||
deleteEstimatePoint: () => Promise<void>;
|
||||
updateEstimatePoint: (payload: IEstimateFormData) => Promise<void>;
|
||||
}
|
||||
|
||||
export class EstimatePoint implements IEstimatePoint {
|
||||
@ -40,6 +39,7 @@ export class EstimatePoint implements IEstimatePoint {
|
||||
|
||||
constructor(
|
||||
private store: RootStore,
|
||||
private projectEstimate: IEstimate,
|
||||
private data: IEstimatePointType
|
||||
) {
|
||||
makeObservable(this, {
|
||||
@ -61,7 +61,6 @@ export class EstimatePoint implements IEstimatePoint {
|
||||
asJson: computed,
|
||||
// actions
|
||||
updateEstimatePoint: action,
|
||||
deleteEstimatePoint: action,
|
||||
});
|
||||
this.id = this.data.id;
|
||||
this.key = this.data.key;
|
||||
@ -97,19 +96,10 @@ export class EstimatePoint implements IEstimatePoint {
|
||||
}
|
||||
|
||||
// actions
|
||||
updateEstimatePoint = async () => {
|
||||
updateEstimatePoint = async (payload: IEstimateFormData) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteEstimatePoint = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return;
|
||||
if (!workspaceSlug || !projectId || !this.projectEstimate?.id || !this.id || !payload) return undefined;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import unset from "lodash/unset";
|
||||
import update from "lodash/update";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import {
|
||||
IEstimate as IEstimateType,
|
||||
@ -7,6 +9,7 @@ import {
|
||||
IProject,
|
||||
IWorkspace,
|
||||
TEstimateType,
|
||||
IEstimateFormData,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { EstimateService } from "@/services/project/estimate.service";
|
||||
@ -27,10 +30,9 @@ export interface IEstimate extends IEstimateType {
|
||||
asJson: IEstimateType;
|
||||
EstimatePointIds: string[] | undefined;
|
||||
estimatePointById: (estimateId: string) => IEstimatePointType | undefined;
|
||||
// helper actions
|
||||
// actions
|
||||
updateEstimate: (data: Partial<IEstimateType>) => Promise<IEstimateType | undefined>;
|
||||
deleteEstimatePoint: (data: Partial<IEstimateType>) => Promise<void>;
|
||||
updateEstimatePointSorting: (payload: IEstimateFormData) => Promise<void>;
|
||||
deleteEstimatePoint: (estimatePointId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class Estimate implements IEstimate {
|
||||
@ -80,7 +82,7 @@ export class Estimate implements IEstimate {
|
||||
asJson: computed,
|
||||
EstimatePointIds: computed,
|
||||
// actions
|
||||
updateEstimate: action,
|
||||
updateEstimatePointSorting: action,
|
||||
deleteEstimatePoint: action,
|
||||
});
|
||||
this.id = this.data.id;
|
||||
@ -99,7 +101,7 @@ export class Estimate implements IEstimate {
|
||||
|
||||
this.data.points?.forEach((estimationPoint) => {
|
||||
if (estimationPoint.id)
|
||||
set(this.estimatePoints, [estimationPoint.id], new EstimatePoint(this.store, estimationPoint));
|
||||
set(this.estimatePoints, [estimationPoint.id], new EstimatePoint(this.store, this.data, estimationPoint));
|
||||
});
|
||||
// service
|
||||
this.service = new EstimateService();
|
||||
@ -141,26 +143,37 @@ export class Estimate implements IEstimate {
|
||||
});
|
||||
|
||||
// actions
|
||||
updateEstimate = async (estimatePoint: Partial<IEstimateType>) => {
|
||||
updateEstimatePointSorting = async (payload: IEstimateFormData) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
if (!workspaceSlug || !projectId || !this.id || !payload) return;
|
||||
|
||||
Object.entries(estimatePoint ?? {}).forEach(([key, value]) => {
|
||||
if (!key || !value) return;
|
||||
set(this, key, value);
|
||||
// make update estimation request
|
||||
|
||||
// runInAction(() => {
|
||||
// this.points = payload.estimate_points;
|
||||
// this.data.points = payload.estimate_points;
|
||||
// });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteEstimatePoint = async (estimatePointId: string) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !estimatePointId) return;
|
||||
|
||||
// make delete estimation request
|
||||
|
||||
runInAction(() => {
|
||||
update(this, "points", (estimationPoints = []) =>
|
||||
estimationPoints.filter((point: IEstimatePointType) => point.id !== estimatePointId)
|
||||
);
|
||||
unset(this.estimatePoints, [estimatePointId]);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteEstimatePoint = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import set from "lodash/set";
|
||||
import unset from "lodash/unset";
|
||||
import update from "lodash/update";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { IEstimate as IEstimateType } from "@plane/types";
|
||||
import { IEstimate as IEstimateType, IEstimateFormData } from "@plane/types";
|
||||
// services
|
||||
import { EstimateService } from "@/services/project/estimate.service";
|
||||
// store
|
||||
@ -22,14 +21,23 @@ export interface IProjectEstimateStore {
|
||||
estimates: Record<string, IEstimate>;
|
||||
error: TErrorCodes | undefined;
|
||||
// computed
|
||||
areEstimateEnabledByProjectId: (projectId: string) => boolean;
|
||||
projectEstimateIds: string[] | undefined;
|
||||
estimateIdsByProjectId: (projectId: string) => string[] | undefined;
|
||||
estimateById: (estimateId: string) => IEstimate | undefined;
|
||||
// actions
|
||||
getWorkspaceAllEstimates: () => Promise<IEstimateType[] | undefined>;
|
||||
getAllEstimates: () => Promise<IEstimateType[] | undefined>;
|
||||
getEstimateById: (estimateId: string) => Promise<IEstimateType | undefined>;
|
||||
createEstimate: (data: Partial<IEstimateType>) => Promise<IEstimateType | undefined>;
|
||||
deleteEstimate: (estimateId: string) => Promise<void>;
|
||||
getWorkspaceEstimates: (workspaceSlug: string, loader?: TEstimateLoader) => Promise<IEstimateType[] | undefined>;
|
||||
getProjectEstimates: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loader?: TEstimateLoader
|
||||
) => Promise<IEstimateType[] | undefined>;
|
||||
getEstimateById: (workspaceSlug: string, projectId: string, estimateId: string) => Promise<IEstimateType | undefined>;
|
||||
createEstimate: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IEstimateFormData
|
||||
) => Promise<IEstimateType | undefined>;
|
||||
}
|
||||
|
||||
export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
@ -49,28 +57,56 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
// computed
|
||||
projectEstimateIds: computed,
|
||||
// actions
|
||||
getWorkspaceAllEstimates: action,
|
||||
getAllEstimates: action,
|
||||
getWorkspaceEstimates: action,
|
||||
getProjectEstimates: action,
|
||||
getEstimateById: action,
|
||||
createEstimate: action,
|
||||
deleteEstimate: action,
|
||||
});
|
||||
// service
|
||||
this.service = new EstimateService();
|
||||
}
|
||||
|
||||
// computed
|
||||
get projectEstimateIds() {
|
||||
/**
|
||||
* @description get estimates are enabled in the project or not
|
||||
* @returns { boolean }
|
||||
*/
|
||||
areEstimateEnabledByProjectId = computedFn((projectId: string) => {
|
||||
if (!projectId) return false;
|
||||
const projectDetails = this.store.projectRoot.project.getProjectById(projectId);
|
||||
if (!projectDetails) return false;
|
||||
return Boolean(projectDetails.estimate) || false;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get all estimate ids for a project
|
||||
* @returns { string[] | undefined }
|
||||
*/
|
||||
get projectEstimateIds(): string[] | undefined {
|
||||
const { projectId } = this.store.router;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
const projectEstimatesIds = Object.values(this.estimates || {})
|
||||
.filter((p) => p.project === projectId)
|
||||
.map((p) => p.id && p != undefined) as string[];
|
||||
|
||||
return projectEstimatesIds ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get all estimate ids for a project
|
||||
* @returns { string[] | undefined }
|
||||
*/
|
||||
estimateIdsByProjectId = computedFn((projectId: string) => {
|
||||
if (!projectId) return undefined;
|
||||
const projectEstimatesIds = Object.values(this.estimates || {})
|
||||
.filter((p) => p.project === projectId)
|
||||
.map((p) => p.id && p != undefined) as string[];
|
||||
return projectEstimatesIds ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get estimate by id
|
||||
* @returns { IEstimate | undefined }
|
||||
*/
|
||||
estimateById = computedFn((estimateId: string) => {
|
||||
if (!estimateId) return undefined;
|
||||
return this.estimates[estimateId] ?? undefined;
|
||||
@ -78,24 +114,29 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetch all estimates for a project
|
||||
* @description fetch all estimates for a workspace
|
||||
* @returns { IEstimateType[] | undefined }
|
||||
*/
|
||||
getWorkspaceAllEstimates = async (): Promise<IEstimateType[] | undefined> => {
|
||||
getWorkspaceEstimates = async (
|
||||
workspaceSlug: string,
|
||||
loader: TEstimateLoader = "mutation-loader"
|
||||
): Promise<IEstimateType[] | undefined> => {
|
||||
try {
|
||||
const { workspaceSlug } = this.store.router;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
this.error = undefined;
|
||||
const estimates = await this.service.fetchWorkspacesList(workspaceSlug);
|
||||
if (!this.projectEstimateIds) this.loader = loader ? loader : "init-loader";
|
||||
|
||||
if (estimates && estimates.length > 0)
|
||||
const estimates = await this.service.fetchWorkspaceEstimates(workspaceSlug);
|
||||
if (estimates && estimates.length > 0) {
|
||||
runInAction(() => {
|
||||
estimates.forEach((estimate) => {
|
||||
if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return estimates;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Error fetching estimates",
|
||||
@ -103,21 +144,31 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
}
|
||||
};
|
||||
|
||||
getAllEstimates = async (): Promise<IEstimateType[] | undefined> => {
|
||||
/**
|
||||
* @description fetch all estimates for a project
|
||||
* @returns { IEstimateType[] | undefined }
|
||||
*/
|
||||
getProjectEstimates = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loader: TEstimateLoader = "mutation-loader"
|
||||
): Promise<IEstimateType[] | undefined> => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
this.error = undefined;
|
||||
const estimates = await this.service.fetchAll(workspaceSlug, projectId);
|
||||
if (!this.projectEstimateIds) this.loader = loader ? loader : "init-loader";
|
||||
|
||||
if (estimates && estimates.length > 0)
|
||||
const estimates = await this.service.fetchProjectEstimates(workspaceSlug, projectId);
|
||||
if (estimates && estimates.length > 0) {
|
||||
runInAction(() => {
|
||||
estimates.forEach((estimate) => {
|
||||
if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return estimates;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Error fetching estimates",
|
||||
@ -130,19 +181,24 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
* @param { string } estimateId
|
||||
* @returns IEstimateType | undefined
|
||||
*/
|
||||
getEstimateById = async (estimateId: string): Promise<IEstimateType | undefined> => {
|
||||
getEstimateById = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
estimateId: string
|
||||
): Promise<IEstimateType | undefined> => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
this.error = undefined;
|
||||
const estimate = await this.service.fetchById(workspaceSlug, projectId, estimateId);
|
||||
|
||||
if (estimate && estimate.id)
|
||||
const estimate = await this.service.fetchEstimateById(workspaceSlug, projectId, estimateId);
|
||||
if (estimate) {
|
||||
runInAction(() => {
|
||||
if (estimate.id)
|
||||
update(this.estimates, [estimate.id], (estimateStore) => {
|
||||
if (estimateStore) estimateStore.updateEstimate(estimate);
|
||||
else return new Estimate(this.store, estimate);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return estimate;
|
||||
} catch (error) {
|
||||
@ -155,48 +211,30 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||
|
||||
/**
|
||||
* @description create an estimate for a project
|
||||
* @param { Partial<IEstimateType> } data
|
||||
* @param { Partial<IEstimateType> } payload
|
||||
* @returns
|
||||
*/
|
||||
createEstimate = async (data: Partial<IEstimateType>): Promise<IEstimateType | undefined> => {
|
||||
createEstimate = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
payload: IEstimateFormData
|
||||
): Promise<IEstimateType | undefined> => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
this.error = undefined;
|
||||
const estimate = await this.service.create(workspaceSlug, projectId, data);
|
||||
|
||||
if (estimate && estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
|
||||
const estimate = await this.service.createEstimate(workspaceSlug, projectId, payload);
|
||||
if (estimate) {
|
||||
runInAction(() => {
|
||||
if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate));
|
||||
});
|
||||
}
|
||||
|
||||
return estimate;
|
||||
} catch (error) {
|
||||
console.error("Error creating estimate");
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Error creating estimate",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description delete an estimate for a project
|
||||
* @param { string } estimateId
|
||||
* @returns void
|
||||
*/
|
||||
deleteEstimate = async (estimateId: string): Promise<void> => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
this.error = undefined;
|
||||
await this.service.remove(workspaceSlug, projectId, estimateId);
|
||||
unset(this.estimates, [estimateId]);
|
||||
} catch (error) {
|
||||
console.error("Error deleting estimate");
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Error deleting estimate",
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -346,7 +346,10 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
||||
// fetching other project modules
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.module.fetchModules(workspaceSlug, projectId);
|
||||
// fetching other project estimates
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.estimate.fetchProjectEstimates(workspaceSlug, projectId);
|
||||
this.rootIssueDetailStore.rootIssueStore.rootStore.projectEstimate.getProjectEstimates(
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -4,7 +4,6 @@ import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.sto
|
||||
import { CycleStore, ICycleStore } from "./cycle.store";
|
||||
import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store";
|
||||
import { DashboardStore, IDashboardStore } from "./dashboard.store";
|
||||
import { EstimateStore, IEstimateStore } from "./estimate.store";
|
||||
import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store";
|
||||
import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store";
|
||||
import { GlobalViewStore, IGlobalViewStore } from "./global-view.store";
|
||||
@ -39,7 +38,6 @@ export class RootStore {
|
||||
issue: IIssueRootStore;
|
||||
state: IStateStore;
|
||||
label: ILabelStore;
|
||||
estimate: IEstimateStore;
|
||||
dashboard: IDashboardStore;
|
||||
projectPages: IProjectPageStore;
|
||||
router: IRouterStore;
|
||||
@ -66,7 +64,6 @@ export class RootStore {
|
||||
this.issue = new IssueRootStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.estimate = new EstimateStore(this);
|
||||
this.dashboard = new DashboardStore(this);
|
||||
this.commandPalette = new CommandPaletteStore();
|
||||
this.theme = new ThemeStore(this);
|
||||
@ -95,7 +92,6 @@ export class RootStore {
|
||||
this.issue = new IssueRootStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.estimate = new EstimateStore(this);
|
||||
this.dashboard = new DashboardStore(this);
|
||||
this.router = new RouterStore();
|
||||
this.commandPalette = new CommandPaletteStore();
|
||||
|
Loading…
Reference in New Issue
Block a user