From 0af70136a90b59b5aa8ada97a60dff9ff0f98abc Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Thu, 23 May 2024 13:41:30 +0530 Subject: [PATCH 01/59] Move code from EE to CE repo --- packages/types/src/estimate.d.ts | 54 ++--- packages/ui/src/index.ts | 3 +- .../src/radio-input/radio-input.stories.tsx | 36 +++ packages/ui/src/radio-input/radio-input.tsx | 46 ++++ packages/ui/src/typography/index.tsx | 1 + packages/ui/src/typography/sub-heading.tsx | 15 ++ web/components/radio-group/index.tsx | 1 + web/components/radio-group/radio-group.tsx | 66 ++++++ web/components/sortable/draggable.tsx | 46 ++++ web/components/sortable/sortable.tsx | 49 +++++ web/constants/estimates.ts | 95 ++++++++ web/ee/components/estimates/create/index.ts | 3 + web/ee/components/estimates/create/modal.tsx | 101 +++++++++ .../components/estimates/create/stage-one.tsx | 59 +++++ .../components/estimates/create/stage-two.tsx | 80 +++++++ web/ee/components/estimates/delete/index.ts | 1 + web/ee/components/estimates/delete/modal.tsx | 24 ++ .../components/estimates/estimate-disable.tsx | 18 ++ web/ee/components/estimates/estimate-item.tsx | 90 ++++++++ .../components/estimates/estimate-search.tsx | 9 + web/ee/components/estimates/index.ts | 11 + .../components/estimates/inline-editable.tsx | 66 ++++++ web/ee/components/estimates/root.tsx | 145 ++++++++++++ web/ee/components/estimates/types.ts | 29 +++ .../estimates/update-estimate-modal.tsx | 207 ++++++++++++++++++ web/ee/components/estimates/update/index.ts | 1 + web/ee/components/estimates/update/modal.tsx | 54 +++++ web/hooks/store/estimates/index.ts | 3 + .../store/estimates/use-estimate-point.ts | 16 ++ web/hooks/store/estimates/use-estimate.ts | 13 ++ .../store/estimates/use-project-estimate.ts | 14 ++ web/hooks/store/index.ts | 2 +- web/hooks/store/use-estimate.ts | 11 - web/store/estimates/estimate-point.ts | 117 ++++++++++ web/store/estimates/estimate.ts | 166 ++++++++++++++ web/store/estimates/project-estimate.store.ts | 200 +++++++++++++++++ web/store/root.store.ts | 3 + 37 files changed, 1817 insertions(+), 38 deletions(-) create mode 100644 packages/ui/src/radio-input/radio-input.stories.tsx create mode 100644 packages/ui/src/radio-input/radio-input.tsx create mode 100644 packages/ui/src/typography/index.tsx create mode 100644 packages/ui/src/typography/sub-heading.tsx create mode 100644 web/components/radio-group/index.tsx create mode 100644 web/components/radio-group/radio-group.tsx create mode 100644 web/components/sortable/draggable.tsx create mode 100644 web/components/sortable/sortable.tsx create mode 100644 web/constants/estimates.ts create mode 100644 web/ee/components/estimates/create/index.ts create mode 100644 web/ee/components/estimates/create/modal.tsx create mode 100644 web/ee/components/estimates/create/stage-one.tsx create mode 100644 web/ee/components/estimates/create/stage-two.tsx create mode 100644 web/ee/components/estimates/delete/index.ts create mode 100644 web/ee/components/estimates/delete/modal.tsx create mode 100644 web/ee/components/estimates/estimate-disable.tsx create mode 100644 web/ee/components/estimates/estimate-item.tsx create mode 100644 web/ee/components/estimates/estimate-search.tsx create mode 100644 web/ee/components/estimates/index.ts create mode 100644 web/ee/components/estimates/inline-editable.tsx create mode 100644 web/ee/components/estimates/root.tsx create mode 100644 web/ee/components/estimates/types.ts create mode 100644 web/ee/components/estimates/update-estimate-modal.tsx create mode 100644 web/ee/components/estimates/update/index.ts create mode 100644 web/ee/components/estimates/update/modal.tsx create mode 100644 web/hooks/store/estimates/index.ts create mode 100644 web/hooks/store/estimates/use-estimate-point.ts create mode 100644 web/hooks/store/estimates/use-estimate.ts create mode 100644 web/hooks/store/estimates/use-project-estimate.ts delete mode 100644 web/hooks/store/use-estimate.ts create mode 100644 web/store/estimates/estimate-point.ts create mode 100644 web/store/estimates/estimate.ts create mode 100644 web/store/estimates/project-estimate.store.ts diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts index 96b584ce1..c2cd2d766 100644 --- a/packages/types/src/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -1,36 +1,40 @@ -export interface IEstimate { - created_at: Date; - created_by: string; - description: string; - id: string; - name: string; - project: string; - project_detail: IProject; - updated_at: Date; - updated_by: string; - points: IEstimatePoint[]; - workspace: string; - workspace_detail: IWorkspace; +export interface IEstimatePoint { + id: string | undefined; + key: number | undefined; + value: string | undefined; + description: string | undefined; + workspace: string | undefined; + project: string | undefined; + estimate: string | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; } -export interface IEstimatePoint { - created_at: string; - created_by: string; - description: string; - estimate: string; - id: string; - key: number; - project: string; - updated_at: string; - updated_by: string; - value: string; - workspace: string; +export type TEstimateType = "categories" | "points" | "time"; + +export interface IEstimate { + id: string | undefined; + name: string | undefined; + description: string | undefined; + type: TEstimateType | undefined; // categories, points, time + points: IEstimatePoint[] | undefined; + workspace: string | undefined; + workspace_detail: IWorkspace | undefined; + project: string | undefined; + project_detail: IProject | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; } export interface IEstimateFormData { estimate: { name: string; description: string; + type: string; }; estimate_points: { id?: string; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index dae012381..5402a43e5 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -13,4 +13,5 @@ export * from "./loader"; export * from "./control-link"; export * from "./toast"; export * from "./drag-handle"; -export * from "./drop-indicator"; \ No newline at end of file +export * from "./typography"; +export * from "./drop-indicator"; diff --git a/packages/ui/src/radio-input/radio-input.stories.tsx b/packages/ui/src/radio-input/radio-input.stories.tsx new file mode 100644 index 000000000..544c2feb4 --- /dev/null +++ b/packages/ui/src/radio-input/radio-input.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { RadioInput } from "./radio-input"; + +const meta: Meta = { + title: "RadioInput", + component: RadioInput, +}; + +export default meta; +type Story = StoryObj; + +const options = [ + { label: "Option 1", value: "option1" }, + { + label: + "A very very long label, lets add some lipsum text and see what happens? May be we don't have to. This is long enough", + value: "option2", + }, + { label: "Option 3", value: "option3" }, +]; + +export const Default: Story = { + args: { + options, + label: "Horizontal Radio Input", + }, +}; + +export const vertical: Story = { + args: { + options, + label: "Vertical Radio Input", + vertical: true, + }, +}; diff --git a/packages/ui/src/radio-input/radio-input.tsx b/packages/ui/src/radio-input/radio-input.tsx new file mode 100644 index 000000000..abafccc1d --- /dev/null +++ b/packages/ui/src/radio-input/radio-input.tsx @@ -0,0 +1,46 @@ +import { Field, Label, Radio, RadioGroup } from "@headlessui/react"; +import React from "react"; + +type RadioInputProps = { + label: string | React.ReactNode | undefined; + ariaLabel?: string; + options: { label: string; value: string; disabled?: boolean }[]; + vertical?: boolean; + selected: string; +}; + +const RadioInput = ({ label: inputLabel, options, vertical, selected, ariaLabel }: RadioInputProps) => { + const wrapperClass = vertical ? "flex flex-col gap-1" : "flex gap-2"; + + const setSelected = (value: string) => { + console.log(value); + }; + + let aria = ariaLabel ? ariaLabel.toLowerCase().replace(" ", "-") : ""; + if (!aria && typeof inputLabel === "string") { + aria = inputLabel.toLowerCase().replace(" ", "-"); + } else { + aria = "radio-input"; + } + + return ( + + +
+ {options.map(({ value, label }) => ( + + + + + + + ))} +
+
+ ); +}; + +export { RadioInput }; diff --git a/packages/ui/src/typography/index.tsx b/packages/ui/src/typography/index.tsx new file mode 100644 index 000000000..0b1b7ffe1 --- /dev/null +++ b/packages/ui/src/typography/index.tsx @@ -0,0 +1 @@ +export * from "./sub-heading"; diff --git a/packages/ui/src/typography/sub-heading.tsx b/packages/ui/src/typography/sub-heading.tsx new file mode 100644 index 000000000..9e7075583 --- /dev/null +++ b/packages/ui/src/typography/sub-heading.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { cn } from "../../helpers"; + +type Props = { + children: React.ReactNode; + className?: string; + noMargin?: boolean; +}; +const SubHeading = ({ children, className, noMargin }: Props) => ( +

+ {children} +

+); + +export { SubHeading }; diff --git a/web/components/radio-group/index.tsx b/web/components/radio-group/index.tsx new file mode 100644 index 000000000..ffd376c16 --- /dev/null +++ b/web/components/radio-group/index.tsx @@ -0,0 +1 @@ +export * from "./radio-group"; diff --git a/web/components/radio-group/radio-group.tsx b/web/components/radio-group/radio-group.tsx new file mode 100644 index 000000000..b9228009b --- /dev/null +++ b/web/components/radio-group/radio-group.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { Field, Label, Radio, RadioGroup } from "@headlessui/react"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type RadioInputProps = { + label: string | React.ReactNode | undefined; + labelClassName?: string; + ariaLabel?: string; + options: { label: string; value: string; disabled?: boolean }[]; + vertical?: boolean; + selected: string; + onChange: (value: string) => void; + className?: string; +}; + +const RadioInput = ({ + label: inputLabel, + labelClassName: inputLabelClassName, + options, + vertical, + selected, + ariaLabel, + onChange, + className, +}: RadioInputProps) => { + const wrapperClass = vertical ? "flex flex-col gap-1" : "flex gap-2"; + + const setSelected = (value: string) => { + onChange(value); + }; + + let aria = ariaLabel ? ariaLabel.toLowerCase().replace(" ", "-") : ""; + if (!aria && typeof inputLabel === "string") { + aria = inputLabel.toLowerCase().replace(" ", "-"); + } else { + aria = "radio-input"; + } + + // return

Hello

; + + return ( + + +
+ {options.map(({ value, label, disabled }) => ( + + + + + + + ))} +
+
+ ); +}; + +export { RadioInput }; diff --git a/web/components/sortable/draggable.tsx b/web/components/sortable/draggable.tsx new file mode 100644 index 000000000..f4271a848 --- /dev/null +++ b/web/components/sortable/draggable.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { isEqual } from "lodash"; +import { cn } from "@/helpers/common.helper"; + +type Props = { + children: React.ReactNode; + data: any; //@todo make this generic +}; +const Draggable = ({ children, data }: Props) => { + const ref = useRef(null); + const [dragging, setDragging] = useState(false); // NEW + const [isDraggedOver, setIsDraggedOver] = useState(false); + + useEffect(() => { + const el = ref.current; + + if (el) { + combine( + draggable({ + element: el, + onDragStart: () => setDragging(true), // NEW + onDrop: () => setDragging(false), // NEW + getInitialData: () => data, + }), + dropTargetForElements({ + element: el, + onDragEnter: () => setIsDraggedOver(true), + onDragLeave: () => setIsDraggedOver(false), + onDrop: () => setIsDraggedOver(false), + canDrop: ({ source }) => !isEqual(source.data, data), + getData: () => data, + }) + ); + } + }, [data]); + + return ( +
+ {children} +
+ ); +}; + +export { Draggable }; diff --git a/web/components/sortable/sortable.tsx b/web/components/sortable/sortable.tsx new file mode 100644 index 000000000..145908e79 --- /dev/null +++ b/web/components/sortable/sortable.tsx @@ -0,0 +1,49 @@ +import React, { Fragment, useEffect } from "react"; +import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + +type Props = { + data: T[]; + render: (item: T, index: number) => React.ReactNode; + onChange: (data: T[]) => void; + keyExtractor: (item: T, index: number) => string; +}; + +const moveItems = (data: T[], source: T, destination: T): T[] => { + const sourceIndex = data.indexOf(source); + const destinationIndex = data.indexOf(destination); + + if (sourceIndex === -1 || destinationIndex === -1) return data; + + const newData = [...data]; + newData.splice(sourceIndex, 1); + newData.splice(destinationIndex, 0, source); + + return newData; +}; + +const Sortable = ({ data, render, onChange, keyExtractor }: Props) => { + useEffect(() => { + const unsubscribe = monitorForElements({ + onDrop({ source, location }) { + const destination = location?.current?.dropTargets[0]; + if (!destination) return; + onChange(moveItems(data, source.data as T, destination.data as T)); + }, + }); + + // Clean up the subscription on unmount + return () => { + if (unsubscribe) unsubscribe(); + }; + }, [data, onChange]); + + return ( + <> + {data.map((item, index) => ( + {render(item, index)} + ))} + + ); +}; + +export { Sortable }; diff --git a/web/constants/estimates.ts b/web/constants/estimates.ts new file mode 100644 index 000000000..0da79fba5 --- /dev/null +++ b/web/constants/estimates.ts @@ -0,0 +1,95 @@ +import { TEstimateSystems } from "@/ee/components/estimates/types"; + +export const ESTIMATE_SYSTEMS: TEstimateSystems = { + points: { + name: "Points", + templates: { + 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 }, + ], + }, + 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 }, + ], + }, + 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 }, + ], + }, + }, + is_available: true, + }, + categories: { + name: "Categories", + templates: { + 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" }, + ], + }, + 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" }, + ], + }, + }, + is_available: true, + }, + time: { + name: "Time", + templates: { + 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 }, + ], + }, + }, + is_available: false, + }, +}; diff --git a/web/ee/components/estimates/create/index.ts b/web/ee/components/estimates/create/index.ts new file mode 100644 index 000000000..6b9fd2095 --- /dev/null +++ b/web/ee/components/estimates/create/index.ts @@ -0,0 +1,3 @@ +export * from "./modal"; +export * from "./stage-one"; +export * from "./stage-two"; diff --git a/web/ee/components/estimates/create/modal.tsx b/web/ee/components/estimates/create/modal.tsx new file mode 100644 index 000000000..fa9f134b5 --- /dev/null +++ b/web/ee/components/estimates/create/modal.tsx @@ -0,0 +1,101 @@ +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"; +// components +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; +// constants +import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; +// ee components +import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/ee/components/estimates"; +// types +import { TEstimateSystemKeys, EEstimateSystem, TEstimateSystemKeyObject } from "@/ee/components/estimates/types"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data?: IEstimate; +}; + +export const CreateEstimateModal: FC = observer((props) => { + // props + const { handleClose, isOpen } = props; + // states + const [estimateSystem, setEstimateSystem] = useState(EEstimateSystem.POINTS); + const [estimatePoints, setEstimatePoints] = useState( + undefined + ); + + const handleUpdatePoints = (newPoints: TEstimateSystemKeyObject[TEstimateSystemKeys] | undefined) => { + const points = cloneDeep(newPoints); + setEstimatePoints(points); + }; + + useEffect(() => { + if (!isOpen) { + setEstimateSystem(EEstimateSystem.POINTS); + setEstimatePoints(undefined); + } + }, [isOpen]); + + // derived values + const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); + + return ( + +
+ {/* heading */} +
+
+ {estimatePoints && ( +
{ + setEstimateSystem(EEstimateSystem.POINTS); + handleUpdatePoints(undefined); + }} + className="flex-shrink-0 cursor-pointer w-5 h-5 flex justify-center items-center" + > + +
+ )} +
New Estimate System
+
+
Step {renderEstimateStepsCount}/2
+
+ + {/* estimate steps */} +
+ {!estimatePoints && ( + + handleUpdatePoints(ESTIMATE_SYSTEMS[estimateSystem].templates[templateType].values) + } + /> + )} + {estimatePoints && ( + + )} +
+ +
+ + {estimatePoints && ( + + )} +
+
+
+ ); +}); diff --git a/web/ee/components/estimates/create/stage-one.tsx b/web/ee/components/estimates/create/stage-one.tsx new file mode 100644 index 000000000..f8206ff26 --- /dev/null +++ b/web/ee/components/estimates/create/stage-one.tsx @@ -0,0 +1,59 @@ +import { FC } from "react"; +// components +import { RadioInput } from "@/components/radio-group"; +// constants +import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; +// types +import { TEstimateSystemKeys } from "@/ee/components/estimates/types"; + +type TEstimateCreateStageOne = { + estimateSystem: TEstimateSystemKeys; + handleEstimateSystem: (value: TEstimateSystemKeys) => void; + handleEstimatePoints: (value: TEstimateSystemKeys) => void; +}; + +export const EstimateCreateStageOne: FC = (props) => { + const { estimateSystem, handleEstimateSystem, handleEstimatePoints } = props; + + const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined; + + if (!currentEstimateSystem) return <>; + return ( +
+
+ { + const currentSystem = system as TEstimateSystemKeys; + return { + label: ESTIMATE_SYSTEMS[currentSystem]?.name, + value: system, + disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available, + }; + })} + label="Choose an estimate system" + labelClassName="text-sm font-medium text-custom-text-200 mb-3" + selected={estimateSystem} + onChange={(value) => handleEstimateSystem(value as TEstimateSystemKeys)} + className="mb-4" + /> +
+
+
Choose a template
+
+ {Object.keys(currentEstimateSystem.templates).map((name) => ( + + ))} +
+
+
+ ); +}; diff --git a/web/ee/components/estimates/create/stage-two.tsx b/web/ee/components/estimates/create/stage-two.tsx new file mode 100644 index 000000000..c645c235c --- /dev/null +++ b/web/ee/components/estimates/create/stage-two.tsx @@ -0,0 +1,80 @@ +import { FC } from "react"; +import { Plus } from "lucide-react"; +import { Button } from "@plane/ui"; +// components +import { Sortable } from "@/components/sortable/sortable"; +// constants +import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; +import { EstimateItem } from "@/ee/components/estimates"; +// types +import { + EEstimateSystem, + TEstimatePointNumeric, + TEstimatePointString, + TEstimateSystemKeyObject, + TEstimateSystemKeys, +} from "@/ee/components/estimates/types"; + +type TEstimateCreateStageTwo = { + estimateSystem: EEstimateSystem; + estimatePoints: TEstimateSystemKeyObject[TEstimateSystemKeys]; + handleEstimatePoints: (value: TEstimateSystemKeyObject[TEstimateSystemKeys]) => void; +}; + +export const EstimateCreateStageTwo: FC = (props) => { + const { estimateSystem, estimatePoints, handleEstimatePoints } = props; + + const currentEstimateSystem = ESTIMATE_SYSTEMS[estimateSystem] || undefined; + + const addNewEstimationPoint = () => { + const currentEstimationPoints = estimatePoints; + if ([EEstimateSystem.POINTS, EEstimateSystem.TIME].includes(estimateSystem)) { + const newEstimationPoint: TEstimatePointNumeric = { + key: currentEstimationPoints.length + 1, + value: 0, + }; + handleEstimatePoints([...currentEstimationPoints, newEstimationPoint] as TEstimatePointNumeric[]); + } + if (estimateSystem === EEstimateSystem.CATEGORIES) { + const newEstimationPoint: TEstimatePointString = { + key: currentEstimationPoints.length + 1, + value: "", + }; + handleEstimatePoints([...currentEstimationPoints, newEstimationPoint] as TEstimatePointString[]); + } + }; + + const deleteEstimationPoint = (index: number) => { + const newEstimationPoints = estimatePoints; + newEstimationPoints.splice(index, 1); + handleEstimatePoints(newEstimationPoints); + }; + + const updatedSortedKeys = (updatedEstimatePoints: TEstimateSystemKeyObject[TEstimateSystemKeys]) => + updatedEstimatePoints.map((item, index) => ({ + ...item, + key: index + 1, + })) as TEstimateSystemKeyObject[TEstimateSystemKeys]; + + return ( +
+ ( + 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() + } + /> + + +
+ ); +}; diff --git a/web/ee/components/estimates/delete/index.ts b/web/ee/components/estimates/delete/index.ts new file mode 100644 index 000000000..031608e25 --- /dev/null +++ b/web/ee/components/estimates/delete/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/web/ee/components/estimates/delete/modal.tsx b/web/ee/components/estimates/delete/modal.tsx new file mode 100644 index 000000000..2ec2008a2 --- /dev/null +++ b/web/ee/components/estimates/delete/modal.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { IEstimate } from "@plane/types"; + +// components +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data?: IEstimate; +}; + +export const DeleteEstimateModal: React.FC = observer((props) => { + const { handleClose, isOpen, data } = props; + + console.log("data", data); + + return ( + +
Delete Estimate Modal
+
+ ); +}); diff --git a/web/ee/components/estimates/estimate-disable.tsx b/web/ee/components/estimates/estimate-disable.tsx new file mode 100644 index 000000000..701d06149 --- /dev/null +++ b/web/ee/components/estimates/estimate-disable.tsx @@ -0,0 +1,18 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; + +type TEstimateDisable = { + workspaceSlug: string; + projectId: string; +}; + +export const EstimateDisable: FC = observer((props) => { + const { workspaceSlug, projectId } = props; + // hooks + + return ( +
+ Estimate Disable {workspaceSlug} {projectId} +
+ ); +}); diff --git a/web/ee/components/estimates/estimate-item.tsx b/web/ee/components/estimates/estimate-item.tsx new file mode 100644 index 000000000..aa9e14b31 --- /dev/null +++ b/web/ee/components/estimates/estimate-item.tsx @@ -0,0 +1,90 @@ +import { Fragment, useRef, useState } from "react"; +import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react"; +import { Select } from "@headlessui/react"; +import { Draggable } from "@/components/sortable/draggable"; +import { InlineEdit } from "./inline-editable"; +import { TEstimatePointNumeric, TEstimatePointString } from "./types"; + +type Props = { + item: TEstimatePointNumeric | TEstimatePointString; + deleteItem: () => void; +}; +const EstimateItem = ({ item, deleteItem }: Props) => { + const { value, id } = item; + + const inputRef = useRef(null); + const [showDeleteUI, setShowDeleteUI] = useState(false); + const [isEditing, setIsEditing] = useState(false); + + const handleDelete = () => { + if (id) { + setShowDeleteUI(true); + } else { + deleteItem(); + } + }; + + const handleEdit = () => { + setIsEditing(true); + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }); + }; + + const handleSave = () => { + if (id) { + // Make the api call to save the estimate point + // Show a spinner + setIsEditing(false); + } + }; + return ( + + {isEditing && ( +
+ {}} + className="border rounded-md border-custom-border-300 p-3 flex-grow" + ref={inputRef} + /> +
+
+ + setIsEditing(false)} /> +
+
+
+ )} + {!isEditing && ( +
+
+ + {!showDeleteUI ? : value} + {showDeleteUI && ( + + + + + setShowDeleteUI(false)} /> + + )} +
+
+ + {!showDeleteUI && } +
+
+ )} +
+ ); +}; + +export { EstimateItem }; diff --git a/web/ee/components/estimates/estimate-search.tsx b/web/ee/components/estimates/estimate-search.tsx new file mode 100644 index 000000000..7c94fc8b8 --- /dev/null +++ b/web/ee/components/estimates/estimate-search.tsx @@ -0,0 +1,9 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; + +export const EstimateSearch: FC = observer(() => { + // hooks + const {} = {}; + + return
Estimate Search
; +}); diff --git a/web/ee/components/estimates/index.ts b/web/ee/components/estimates/index.ts new file mode 100644 index 000000000..ff3d7f02a --- /dev/null +++ b/web/ee/components/estimates/index.ts @@ -0,0 +1,11 @@ +export * from "./root"; +export * from "./estimate-search"; +export * from "./estimate-disable"; +export * from "./estimate-item"; + +// estimate create +export * from "./create"; + +// estimate update + +// estimate delete diff --git a/web/ee/components/estimates/inline-editable.tsx b/web/ee/components/estimates/inline-editable.tsx new file mode 100644 index 000000000..a2ecfa3d7 --- /dev/null +++ b/web/ee/components/estimates/inline-editable.tsx @@ -0,0 +1,66 @@ +import { HTMLInputTypeAttribute, useEffect, useRef, useState } from "react"; +import { Check, X } from "lucide-react"; + +type Props = { + onSave?: (value: string | number) => void; + value: string | number; + inputType?: HTMLInputTypeAttribute; + isEditing?: boolean; +}; +const InlineEdit = ({ + onSave, + value: defaultValue, + inputType = "text", + isEditing: defaultIsEditing = false, +}: Props) => { + const [isEditing, setIsEditing] = useState(defaultIsEditing); + const [value, setValue] = useState(defaultValue); + const wrapperRef = useRef(null); + const inputRef = useRef(null); + + // Add listener to double click on the div + useEffect(() => { + wrapperRef?.current?.addEventListener("dblclick", () => { + setIsEditing(true); + setTimeout(() => { + inputRef?.current?.select(); + }); + }); + }, []); + + const handleSave = () => { + if (value) { + typeof value === "string" && value.trim(); + onSave && onSave(value); + setIsEditing(false); + } + }; + + return ( +
+ {isEditing ? ( +
+ setValue(e.target.value)} + className="flex flex-grow border-custom-border-300 border rounded-sm" + /> + + { + setValue(defaultValue); + setIsEditing(false); + }} + className="w-6 h-6 bg-custom-background-80 rounded-sm" + /> +
+ ) : ( + value + )} +
+ ); +}; + +export { InlineEdit }; diff --git a/web/ee/components/estimates/root.tsx b/web/ee/components/estimates/root.tsx new file mode 100644 index 000000000..ca87857c7 --- /dev/null +++ b/web/ee/components/estimates/root.tsx @@ -0,0 +1,145 @@ +import { FC, useState } from "react"; +import useSWR from "swr"; +import { IEstimate } from "@plane/types"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { EmptyState } from "@/components/empty-state"; +import { DeleteEstimateModal, EstimateListItem } from "@/components/estimates"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +// ee components +import { CreateEstimateModal } from "@/ee/components/estimates"; +// hooks +import { useProject, useProjectEstimates } from "@/hooks/store"; + +type TEstimateRoot = { + workspaceSlug: string; + projectId: string; +}; + +export const EstimateRoot: FC = (props) => { + const { workspaceSlug, projectId } = props; + // hooks + const { updateProject, currentProjectDetails } = useProject(); + const { loader, projectEstimateIds, estimateById, getAllEstimates } = useProjectEstimates(projectId); + // states + const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(true); + const [isEstimateDeleteModalOpen, setIsEstimateDeleteModalOpen] = useState(null); + const [estimateToUpdate, setEstimateToUpdate] = useState(); + + const { isLoading: isSWRLoading } = useSWR( + workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId ? () => getAllEstimates() : null + ); + + 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; + + 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", + }); + }); + }; + + if (!workspaceSlug || !projectId) return <>; + + return ( +
+ {/* modals */} + {/* create modal */} + { + setIsEstimateCreateModalOpen(false); + setEstimateToUpdate(undefined); + }} + /> + {/* edit modal */} + {/* delete modal */} + setIsEstimateDeleteModalOpen(null)} + data={ + null + // getProjectEstimateById(isEstimateDeleteModalOpen!) + } + /> + + {/* handling the estimates list */} + {loader === "init-loader" || isSWRLoading ? ( + + + + + + + ) : ( + <> + {/* header section */} +
+

Estimates

+
+
+ + {currentProjectDetails?.estimate && ( + + )} +
+
+
+ + {/* listing of estimates */} + {!projectEstimateIds || (projectEstimateIds && projectEstimateIds.length <= 0) ? ( +
+ +
+ ) : ( +
+ {projectEstimateIds && + projectEstimateIds.map((estimateId: string) => { + const estimate = estimateById(estimateId); + if (!estimate) return <>; + return ( + editEstimate(estimate)} + deleteEstimate={(estimateId) => setIsEstimateDeleteModalOpen(estimateId)} + /> + ); + })} +
+ )} + + )} +
+ ); +}; diff --git a/web/ee/components/estimates/types.ts b/web/ee/components/estimates/types.ts new file mode 100644 index 000000000..8d98ae725 --- /dev/null +++ b/web/ee/components/estimates/types.ts @@ -0,0 +1,29 @@ +export enum EEstimateSystem { + POINTS = "points", + CATEGORIES = "categories", + TIME = "time", +} +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 TTemplateValues = { + title: string; + values: TEstimateSystemKeyObject[T]; +}; + +export type TEstimateSystem = { + name: string; + templates: Record>; + is_available: boolean; +}; + +export type TEstimateSystems = { + [K in TEstimateSystemKeys]: TEstimateSystem; +}; diff --git a/web/ee/components/estimates/update-estimate-modal.tsx b/web/ee/components/estimates/update-estimate-modal.tsx new file mode 100644 index 000000000..501868d9b --- /dev/null +++ b/web/ee/components/estimates/update-estimate-modal.tsx @@ -0,0 +1,207 @@ +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 = 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(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 ( + +
+ {!points && ( + +
+
+ New Estimate System +
+
+ Step 2/2 +
+
+
console.log("Submitted")}> +
+ ({ label: name, value: name }))} + label="Choose an estimate system" + selected={estimateSystem} + onChange={(value) => setEstimateSystem(value)} + className="mb-4" + /> +
+ Choose a template +
+ {Object.keys(currentEstimateSystem.templates).map((name) => ( + + ))} +
+ + {/* Add modal footer */} +
+
+ )} + + {points && ( +
+
+
+ + New Estimate System +
+
+ Step 2/2 +
+
+ + ( + deleteItem(index)} /> + )} + onChange={(data: number[]) => setPoints(data)} + keyExtractor={(value: number) => value} + /> + +
+ )} + +
+ + {points && ( + + )} +
+
+
+ ); +}); diff --git a/web/ee/components/estimates/update/index.ts b/web/ee/components/estimates/update/index.ts new file mode 100644 index 000000000..031608e25 --- /dev/null +++ b/web/ee/components/estimates/update/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/web/ee/components/estimates/update/modal.tsx b/web/ee/components/estimates/update/modal.tsx new file mode 100644 index 000000000..e5ef26249 --- /dev/null +++ b/web/ee/components/estimates/update/modal.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { IEstimate } from "@plane/types"; +import { Button } from "@plane/ui"; +// components +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; +// types +import { TEstimateSystemKeys, TEstimateSystemKeyObject } from "@/ee/components/estimates/types"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data?: IEstimate; +}; + +export const UpdateEstimateModal: FC = observer((props) => { + // props + const { handleClose, isOpen } = props; + // states + const [estimatePoints, setEstimatePoints] = useState( + undefined + ); + + useEffect(() => { + if (!isOpen) { + setEstimatePoints(undefined); + } + }, [isOpen]); + + // derived values + + return ( + +
+ {/* heading */} +
Heading
+ + {/* estimate steps */} +
Content
+ +
+ + {estimatePoints && ( + + )} +
+
+
+ ); +}); diff --git a/web/hooks/store/estimates/index.ts b/web/hooks/store/estimates/index.ts new file mode 100644 index 000000000..3b22fbb6f --- /dev/null +++ b/web/hooks/store/estimates/index.ts @@ -0,0 +1,3 @@ +export * from "./use-project-estimate"; +export * from "./use-estimate"; +export * from "./use-estimate-point"; diff --git a/web/hooks/store/estimates/use-estimate-point.ts b/web/hooks/store/estimates/use-estimate-point.ts new file mode 100644 index 000000000..156934caa --- /dev/null +++ b/web/hooks/store/estimates/use-estimate-point.ts @@ -0,0 +1,16 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// mobx store +import { IEstimatePoint } from "@/store/estimates/estimate-point"; + +export const useEstimatePoint = ( + estimateId: string | undefined, + estimatePointId: string | undefined +): IEstimatePoint => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useEstimatePoint must be used within StoreProvider"); + if (!estimateId || !estimatePointId) return {} as IEstimatePoint; + + return context.projectEstimate.estimates?.[estimateId]?.estimatePoints?.[estimatePointId] || {}; +}; diff --git a/web/hooks/store/estimates/use-estimate.ts b/web/hooks/store/estimates/use-estimate.ts new file mode 100644 index 000000000..a1c6ad4ef --- /dev/null +++ b/web/hooks/store/estimates/use-estimate.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// mobx store +import { IEstimate } from "@/store/estimates/estimate"; + +export const useEstimate = (estimateId: string | undefined): IEstimate => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useEstimate must be used within StoreProvider"); + if (!estimateId) return {} as IEstimate; + + return context.projectEstimate.estimates?.[estimateId] ?? {}; +}; diff --git a/web/hooks/store/estimates/use-project-estimate.ts b/web/hooks/store/estimates/use-project-estimate.ts new file mode 100644 index 000000000..f4179eee1 --- /dev/null +++ b/web/hooks/store/estimates/use-project-estimate.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +// context +import { StoreContext } from "@/lib/store-context"; +// mobx store +import { IProjectEstimateStore } from "@/store/estimates/project-estimate.store"; + +export const useProjectEstimates = (projectId: string | undefined): 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; +}; diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index bf139d69a..f0b7d5477 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -3,7 +3,6 @@ export * from "./use-cycle-filter"; export * from "./use-cycle"; export * from "./use-event-tracker"; export * from "./use-dashboard"; -export * from "./use-estimate"; export * from "./use-global-view"; export * from "./use-label"; export * from "./use-member"; @@ -32,3 +31,4 @@ export * from "./use-instance"; export * from "./use-app-theme"; export * from "./use-command-palette"; export * from "./use-app-router"; +export * from "./estimates"; diff --git a/web/hooks/store/use-estimate.ts b/web/hooks/store/use-estimate.ts deleted file mode 100644 index a8e65ea75..000000000 --- a/web/hooks/store/use-estimate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -// mobx store -import { StoreContext } from "@/lib/store-context"; -// types -import { IEstimateStore } from "@/store/estimate.store"; - -export const useEstimate = (): IEstimateStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useEstimate must be used within StoreProvider"); - return context.estimate; -}; diff --git a/web/store/estimates/estimate-point.ts b/web/store/estimates/estimate-point.ts new file mode 100644 index 000000000..babc425ea --- /dev/null +++ b/web/store/estimates/estimate-point.ts @@ -0,0 +1,117 @@ +import { action, computed, makeObservable, observable } from "mobx"; +import { IEstimatePoint as IEstimatePointType } from "@plane/types"; +// services +import { EstimateService } from "@/services/project/estimate.service"; +// store +import { RootStore } from "@/store/root.store"; + +type TErrorCodes = { + status: string; + message?: string; +}; + +export interface IEstimatePoint extends IEstimatePointType { + // observables + error: TErrorCodes | undefined; + // computed + asJson: IEstimatePointType; + // actions + updateEstimatePoint: () => Promise; + deleteEstimatePoint: () => Promise; +} + +export class EstimatePoint implements IEstimatePoint { + // data model observables + id: string | undefined = undefined; + key: number | undefined = undefined; + value: string | undefined = undefined; + description: string | undefined = undefined; + workspace: string | undefined = undefined; + project: string | undefined = undefined; + estimate: string | undefined = undefined; + created_at: Date | undefined = undefined; + updated_at: Date | undefined = undefined; + created_by: string | undefined = undefined; + updated_by: string | undefined = undefined; + // observables + error: TErrorCodes | undefined = undefined; + // service + service: EstimateService; + + constructor( + private store: RootStore, + private data: IEstimatePointType + ) { + makeObservable(this, { + // data model observables + id: observable.ref, + key: observable.ref, + value: observable.ref, + description: observable.ref, + workspace: observable.ref, + project: observable.ref, + estimate: observable.ref, + created_at: observable.ref, + updated_at: observable.ref, + created_by: observable.ref, + updated_by: observable.ref, + // observables + error: observable.ref, + // computed + asJson: computed, + // actions + updateEstimatePoint: action, + deleteEstimatePoint: action, + }); + this.id = this.data.id; + this.key = this.data.key; + this.value = this.data.value; + this.description = this.data.description; + this.workspace = this.data.workspace; + this.project = this.data.project; + this.estimate = this.data.estimate; + this.created_at = this.data.created_at; + this.updated_at = this.data.updated_at; + this.created_by = this.data.created_by; + this.updated_by = this.data.updated_by; + + // service + this.service = new EstimateService(); + } + + // computed + get asJson() { + return { + id: this.id, + key: this.key, + value: this.value, + description: this.description, + workspace: this.workspace, + project: this.project, + estimate: this.estimate, + created_at: this.created_at, + updated_at: this.updated_at, + created_by: this.created_by, + updated_by: this.updated_by, + }; + } + + // actions + updateEstimatePoint = async () => { + 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; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/estimates/estimate.ts b/web/store/estimates/estimate.ts new file mode 100644 index 000000000..527a2aaa6 --- /dev/null +++ b/web/store/estimates/estimate.ts @@ -0,0 +1,166 @@ +import set from "lodash/set"; +import { action, computed, makeObservable, observable } from "mobx"; +import { computedFn } from "mobx-utils"; +import { + IEstimate as IEstimateType, + IEstimatePoint as IEstimatePointType, + IProject, + IWorkspace, + TEstimateType, +} from "@plane/types"; +// services +import { EstimateService } from "@/services/project/estimate.service"; +// store +import { IEstimatePoint, EstimatePoint } from "@/store/estimates/estimate-point"; +import { RootStore } from "@/store/root.store"; + +type TErrorCodes = { + status: string; + message?: string; +}; + +export interface IEstimate extends IEstimateType { + // observables + error: TErrorCodes | undefined; + estimatePoints: Record; + // computed + asJson: IEstimateType; + EstimatePointIds: string[] | undefined; + estimatePointById: (estimateId: string) => IEstimatePointType | undefined; + // helper actions + // actions + updateEstimate: (data: Partial) => Promise; + deleteEstimatePoint: (data: Partial) => Promise; +} + +export class Estimate implements IEstimate { + // data model observables + id: string | undefined = undefined; + name: string | undefined = undefined; + description: string | undefined = undefined; + type: TEstimateType | undefined = undefined; + points: IEstimatePointType[] | undefined = undefined; + workspace: string | undefined = undefined; + workspace_detail: IWorkspace | undefined = undefined; + project: string | undefined = undefined; + project_detail: IProject | undefined = undefined; + created_at: Date | undefined = undefined; + updated_at: Date | undefined = undefined; + created_by: string | undefined = undefined; + updated_by: string | undefined = undefined; + // observables + error: TErrorCodes | undefined = undefined; + estimatePoints: Record = {}; + // service + service: EstimateService; + + constructor( + private store: RootStore, + private data: IEstimateType + ) { + makeObservable(this, { + // data model observables + id: observable.ref, + name: observable.ref, + description: observable.ref, + type: observable.ref, + points: observable, + workspace: observable.ref, + workspace_detail: observable, + project: observable.ref, + project_detail: observable, + created_at: observable.ref, + updated_at: observable.ref, + created_by: observable.ref, + updated_by: observable.ref, + // observables + error: observable.ref, + estimatePoints: observable, + // computed + asJson: computed, + EstimatePointIds: computed, + // actions + updateEstimate: action, + deleteEstimatePoint: action, + }); + this.id = this.data.id; + this.name = this.data.name; + this.description = this.data.description; + this.type = this.data.type; + this.points = this.data.points; + this.workspace = this.data.workspace; + this.workspace_detail = this.data.workspace_detail; + this.project = this.data.project; + this.project_detail = this.data.project_detail; + this.created_at = this.data.created_at; + this.updated_at = this.data.updated_at; + this.created_by = this.data.created_by; + this.updated_by = this.data.updated_by; + + this.data.points?.forEach((estimationPoint) => { + if (estimationPoint.id) + set(this.estimatePoints, [estimationPoint.id], new EstimatePoint(this.store, estimationPoint)); + }); + // service + this.service = new EstimateService(); + } + + // computed + get asJson() { + return { + id: this.id, + name: this.name, + description: this.description, + type: this.type, + points: this.points, + workspace: this.workspace, + workspace_detail: this.workspace_detail, + project: this.project, + project_detail: this.project_detail, + created_at: this.created_at, + updated_at: this.updated_at, + created_by: this.created_by, + updated_by: this.updated_by, + }; + } + + get EstimatePointIds() { + const { estimatePoints } = this; + if (!estimatePoints) return undefined; + + const estimatePointIds = Object.values(estimatePoints) + .map((point) => point.estimate && this.id) + .filter((id) => id) as string[]; + + return estimatePointIds ?? undefined; + } + + estimatePointById = computedFn((estimatePointId: string) => { + if (!estimatePointId) return undefined; + return this.estimatePoints[estimatePointId] ?? undefined; + }); + + // actions + updateEstimate = async (estimatePoint: Partial) => { + try { + const { workspaceSlug, projectId } = this.store.router; + if (!workspaceSlug || !projectId || !this.id) return undefined; + + Object.entries(estimatePoint ?? {}).forEach(([key, value]) => { + if (!key || !value) return; + set(this, key, value); + }); + } catch (error) { + throw error; + } + }; + + deleteEstimatePoint = async () => { + try { + const { workspaceSlug, projectId } = this.store.router; + if (!workspaceSlug || !projectId || !this.id) return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/estimates/project-estimate.store.ts b/web/store/estimates/project-estimate.store.ts new file mode 100644 index 000000000..760dfc59b --- /dev/null +++ b/web/store/estimates/project-estimate.store.ts @@ -0,0 +1,200 @@ +import set from "lodash/set"; +import unset from "lodash/unset"; +import update from "lodash/update"; +import { action, computed, makeObservable, observable } from "mobx"; +import { computedFn } from "mobx-utils"; +import { IEstimate as IEstimateType } from "@plane/types"; +// services +import { EstimateService } from "@/services/project/estimate.service"; +// store +import { IEstimate, Estimate } from "@/store/estimates/estimate"; +import { RootStore } from "@/store/root.store"; + +type TEstimateLoader = "init-loader" | "mutation-loader" | undefined; +type TErrorCodes = { + status: string; + message?: string; +}; + +export interface IProjectEstimateStore { + // observables + loader: TEstimateLoader; + estimates: Record; + error: TErrorCodes | undefined; + // computed + projectEstimateIds: string[] | undefined; + estimateById: (estimateId: string) => IEstimate | undefined; + // actions + getAllEstimates: () => Promise; + getEstimateById: (estimateId: string) => Promise; + createEstimate: (data: Partial) => Promise; + deleteEstimate: (estimateId: string) => Promise; +} + +export class ProjectEstimateStore implements IProjectEstimateStore { + // observables + loader: TEstimateLoader = undefined; + estimates: Record = {}; // estimate_id -> estimate + error: TErrorCodes | undefined = undefined; + // service + service: EstimateService; + + constructor(private store: RootStore) { + makeObservable(this, { + // observables + loader: observable.ref, + estimates: observable, + error: observable, + // computed + projectEstimateIds: computed, + // actions + getAllEstimates: action, + getEstimateById: action, + createEstimate: action, + deleteEstimate: action, + }); + // service + this.service = new EstimateService(); + } + + // computed + get projectEstimateIds() { + 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; + } + + estimateById = computedFn((estimateId: string) => { + if (!estimateId) return undefined; + return this.estimates[estimateId] ?? undefined; + }); + + // actions + /** + * @description fetch all estimates for a project + * @returns { IEstimateType[] | undefined } + */ + async getWorkspaceAllEstimates(): Promise { + try { + const { workspaceSlug } = this.store.router; + if (!workspaceSlug) return; + + this.error = undefined; + const estimates = await this.service.fetchWorkspacesList(workspaceSlug); + + if (estimates && estimates.length > 0) + estimates.forEach((estimate) => { + if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate)); + }); + + return estimates; + } catch (error) { + this.error = { + status: "error", + message: "Error fetching estimates", + }; + } + } + + async getAllEstimates(): Promise { + try { + const { workspaceSlug, projectId } = this.store.router; + if (!workspaceSlug || !projectId) return; + + this.error = undefined; + const estimates = await this.service.fetchAll(workspaceSlug, projectId); + + if (estimates && estimates.length > 0) + estimates.forEach((estimate) => { + if (estimate.id) set(this.estimates, [estimate.id], new Estimate(this.store, estimate)); + }); + + return estimates; + } catch (error) { + this.error = { + status: "error", + message: "Error fetching estimates", + }; + } + } + + /** + * @description update an estimate for a project + * @param { string } estimateId + * @returns IEstimateType | undefined + */ + async getEstimateById(estimateId: string): Promise { + 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) + update(this.estimates, [estimate.id], (estimateStore) => { + if (estimateStore) estimateStore.updateEstimate(estimate); + else return new Estimate(this.store, estimate); + }); + + return estimate; + } catch (error) { + this.error = { + status: "error", + message: "Error fetching estimate by id", + }; + } + } + + /** + * @description create an estimate for a project + * @param { Partial } data + * @returns + */ + async createEstimate(data: Partial): Promise { + 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)); + + 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 + */ + async deleteEstimate(estimateId: string): Promise { + 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", + }; + } + } +} diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 4e6a74815..45fb2fd21 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -5,6 +5,7 @@ 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"; import { IProjectInboxStore, ProjectInboxStore } from "./inbox/project-inbox.store"; @@ -48,6 +49,7 @@ export class RootStore { instance: IInstanceStore; user: IUserStore; projectInbox: IProjectInboxStore; + projectEstimate: IProjectEstimateStore; constructor() { this.router = new RouterStore(); @@ -74,6 +76,7 @@ export class RootStore { this.projectInbox = new ProjectInboxStore(this); this.projectPages = new ProjectPageStore(this); this.theme = new ThemeStore(this); + this.projectEstimate = new ProjectEstimateStore(this); } resetOnSignOut() { From d6325320e9101717dd68715608083a0ef9917e51 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 23 May 2024 15:37:25 +0530 Subject: [PATCH 02/59] chore: folder structure updates --- .../create-update-estimate-modal.tsx | 0 .../delete-estimate-modal.tsx | 0 .../estimate-list-item.tsx | 0 .../estimates-list.tsx | 2 +- web/components/estimates-legacy/index.ts | 4 + .../components/estimates/create/index.ts | 0 .../components/estimates/create/modal.tsx | 5 +- .../components/estimates/create/stage-one.tsx | 2 +- .../components/estimates/create/stage-two.tsx | 11 +- .../components/estimates/delete/index.ts | 0 .../components/estimates/delete/modal.tsx | 0 web/components/estimates/empty-screen.tsx | 39 ++++++ .../components/estimates/estimate-disable.tsx | 0 .../components/estimates/estimate-item.tsx | 0 .../components/estimates/estimate-search.tsx | 0 web/components/estimates/index.ts | 16 ++- .../components/estimates/inline-editable.tsx | 0 web/{ee => }/components/estimates/root.tsx | 53 ++++---- web/{ee => }/components/estimates/types.ts | 0 .../estimates/update-estimate-modal.tsx | 0 .../components/estimates/update/index.ts | 0 .../components/estimates/update/modal.tsx | 2 +- web/components/radio-group/radio-group.tsx | 2 - web/constants/estimates.ts | 2 +- web/ee/components/estimates/index.ts | 11 -- web/package.json | 4 +- .../[projectId]/settings/estimates.tsx | 15 ++- web/services/project/estimate.service.ts | 79 ++++++++++++ web/services/project/index.ts | 1 + web/store/estimates/project-estimate.store.ts | 22 ++-- yarn.lock | 114 ++++++++++++++++-- 31 files changed, 300 insertions(+), 84 deletions(-) rename web/components/{estimates => estimates-legacy}/create-update-estimate-modal.tsx (100%) rename web/components/{estimates => estimates-legacy}/delete-estimate-modal.tsx (100%) rename web/components/{estimates => estimates-legacy}/estimate-list-item.tsx (100%) rename web/components/{estimates => estimates-legacy}/estimates-list.tsx (98%) create mode 100644 web/components/estimates-legacy/index.ts rename web/{ee => }/components/estimates/create/index.ts (100%) rename web/{ee => }/components/estimates/create/modal.tsx (97%) rename web/{ee => }/components/estimates/create/stage-one.tsx (97%) rename web/{ee => }/components/estimates/create/stage-two.tsx (96%) rename web/{ee => }/components/estimates/delete/index.ts (100%) rename web/{ee => }/components/estimates/delete/modal.tsx (100%) create mode 100644 web/components/estimates/empty-screen.tsx rename web/{ee => }/components/estimates/estimate-disable.tsx (100%) rename web/{ee => }/components/estimates/estimate-item.tsx (100%) rename web/{ee => }/components/estimates/estimate-search.tsx (100%) rename web/{ee => }/components/estimates/inline-editable.tsx (100%) rename web/{ee => }/components/estimates/root.tsx (93%) rename web/{ee => }/components/estimates/types.ts (100%) rename web/{ee => }/components/estimates/update-estimate-modal.tsx (100%) rename web/{ee => }/components/estimates/update/index.ts (100%) rename web/{ee => }/components/estimates/update/modal.tsx (98%) delete mode 100644 web/ee/components/estimates/index.ts create mode 100644 web/services/project/estimate.service.ts diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates-legacy/create-update-estimate-modal.tsx similarity index 100% rename from web/components/estimates/create-update-estimate-modal.tsx rename to web/components/estimates-legacy/create-update-estimate-modal.tsx diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates-legacy/delete-estimate-modal.tsx similarity index 100% rename from web/components/estimates/delete-estimate-modal.tsx rename to web/components/estimates-legacy/delete-estimate-modal.tsx diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates-legacy/estimate-list-item.tsx similarity index 100% rename from web/components/estimates/estimate-list-item.tsx rename to web/components/estimates-legacy/estimate-list-item.tsx diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates-legacy/estimates-list.tsx similarity index 98% rename from web/components/estimates/estimates-list.tsx rename to web/components/estimates-legacy/estimates-list.tsx index fc54cd654..5103726e5 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates-legacy/estimates-list.tsx @@ -5,7 +5,7 @@ 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"; +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"; diff --git a/web/components/estimates-legacy/index.ts b/web/components/estimates-legacy/index.ts new file mode 100644 index 000000000..2f4733055 --- /dev/null +++ b/web/components/estimates-legacy/index.ts @@ -0,0 +1,4 @@ +export * from "./create-update-estimate-modal"; +export * from "./delete-estimate-modal"; +export * from "./estimate-list-item"; +export * from "./estimates-list"; diff --git a/web/ee/components/estimates/create/index.ts b/web/components/estimates/create/index.ts similarity index 100% rename from web/ee/components/estimates/create/index.ts rename to web/components/estimates/create/index.ts diff --git a/web/ee/components/estimates/create/modal.tsx b/web/components/estimates/create/modal.tsx similarity index 97% rename from web/ee/components/estimates/create/modal.tsx rename to web/components/estimates/create/modal.tsx index fa9f134b5..bdc635882 100644 --- a/web/ee/components/estimates/create/modal.tsx +++ b/web/components/estimates/create/modal.tsx @@ -6,12 +6,11 @@ import { IEstimate } from "@plane/types"; import { Button } 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"; // constants import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; // ee components -import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/ee/components/estimates"; -// types -import { TEstimateSystemKeys, EEstimateSystem, TEstimateSystemKeyObject } from "@/ee/components/estimates/types"; type Props = { isOpen: boolean; diff --git a/web/ee/components/estimates/create/stage-one.tsx b/web/components/estimates/create/stage-one.tsx similarity index 97% rename from web/ee/components/estimates/create/stage-one.tsx rename to web/components/estimates/create/stage-one.tsx index f8206ff26..ccd8023cd 100644 --- a/web/ee/components/estimates/create/stage-one.tsx +++ b/web/components/estimates/create/stage-one.tsx @@ -4,7 +4,7 @@ import { RadioInput } from "@/components/radio-group"; // constants import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; // types -import { TEstimateSystemKeys } from "@/ee/components/estimates/types"; +import { TEstimateSystemKeys } from "@/components/estimates/types"; type TEstimateCreateStageOne = { estimateSystem: TEstimateSystemKeys; diff --git a/web/ee/components/estimates/create/stage-two.tsx b/web/components/estimates/create/stage-two.tsx similarity index 96% rename from web/ee/components/estimates/create/stage-two.tsx rename to web/components/estimates/create/stage-two.tsx index c645c235c..74a1de0ae 100644 --- a/web/ee/components/estimates/create/stage-two.tsx +++ b/web/components/estimates/create/stage-two.tsx @@ -2,18 +2,17 @@ import { FC } from "react"; import { Plus } from "lucide-react"; import { Button } from "@plane/ui"; // components -import { Sortable } from "@/components/sortable/sortable"; -// constants -import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; -import { EstimateItem } from "@/ee/components/estimates"; -// types +import { EstimateItem } from "@/components/estimates"; import { EEstimateSystem, TEstimatePointNumeric, TEstimatePointString, TEstimateSystemKeyObject, TEstimateSystemKeys, -} from "@/ee/components/estimates/types"; +} from "@/components/estimates/types"; +import { Sortable } from "@/components/sortable/sortable"; +// constants +import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; type TEstimateCreateStageTwo = { estimateSystem: EEstimateSystem; diff --git a/web/ee/components/estimates/delete/index.ts b/web/components/estimates/delete/index.ts similarity index 100% rename from web/ee/components/estimates/delete/index.ts rename to web/components/estimates/delete/index.ts diff --git a/web/ee/components/estimates/delete/modal.tsx b/web/components/estimates/delete/modal.tsx similarity index 100% rename from web/ee/components/estimates/delete/modal.tsx rename to web/components/estimates/delete/modal.tsx diff --git a/web/components/estimates/empty-screen.tsx b/web/components/estimates/empty-screen.tsx new file mode 100644 index 000000000..03f21ab8f --- /dev/null +++ b/web/components/estimates/empty-screen.tsx @@ -0,0 +1,39 @@ +import { FC } from "react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { Button } from "@plane/ui"; + +type TEstimateEmptyScreen = { + onButtonClick: () => void; +}; + +export const EstimateEmptyScreen: FC = (props) => { + // props + const { onButtonClick } = props; + const { resolvedTheme } = useTheme(); + + const emptyScreenImage = `/empty-state/project-settings/estimates-${ + resolvedTheme === "light" ? "light" : "dark" + }.webp`; + + return ( +
+
+ Empty estimate image +
+
+

No estimate systems yet

+

Explain estimates system here

+
+
+ +
+
+ ); +}; diff --git a/web/ee/components/estimates/estimate-disable.tsx b/web/components/estimates/estimate-disable.tsx similarity index 100% rename from web/ee/components/estimates/estimate-disable.tsx rename to web/components/estimates/estimate-disable.tsx diff --git a/web/ee/components/estimates/estimate-item.tsx b/web/components/estimates/estimate-item.tsx similarity index 100% rename from web/ee/components/estimates/estimate-item.tsx rename to web/components/estimates/estimate-item.tsx diff --git a/web/ee/components/estimates/estimate-search.tsx b/web/components/estimates/estimate-search.tsx similarity index 100% rename from web/ee/components/estimates/estimate-search.tsx rename to web/components/estimates/estimate-search.tsx diff --git a/web/components/estimates/index.ts b/web/components/estimates/index.ts index 2f4733055..33d533057 100644 --- a/web/components/estimates/index.ts +++ b/web/components/estimates/index.ts @@ -1,4 +1,12 @@ -export * from "./create-update-estimate-modal"; -export * from "./delete-estimate-modal"; -export * from "./estimate-list-item"; -export * from "./estimates-list"; +export * from "./root"; +export * from "./empty-screen"; +export * from "./estimate-search"; +export * from "./estimate-disable"; +export * from "./estimate-item"; + +// estimate create +export * from "./create"; + +// estimate update + +// estimate delete diff --git a/web/ee/components/estimates/inline-editable.tsx b/web/components/estimates/inline-editable.tsx similarity index 100% rename from web/ee/components/estimates/inline-editable.tsx rename to web/components/estimates/inline-editable.tsx diff --git a/web/ee/components/estimates/root.tsx b/web/components/estimates/root.tsx similarity index 93% rename from web/ee/components/estimates/root.tsx rename to web/components/estimates/root.tsx index ca87857c7..e808243d7 100644 --- a/web/ee/components/estimates/root.tsx +++ b/web/components/estimates/root.tsx @@ -4,11 +4,11 @@ import { IEstimate } from "@plane/types"; import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components import { EmptyState } from "@/components/empty-state"; -import { DeleteEstimateModal, EstimateListItem } from "@/components/estimates"; +import { CreateEstimateModal, EstimateEmptyScreen } from "@/components/estimates"; +import { DeleteEstimateModal, EstimateListItem } from "@/components/estimates-legacy"; // constants import { EmptyStateType } from "@/constants/empty-state"; // ee components -import { CreateEstimateModal } from "@/ee/components/estimates"; // hooks import { useProject, useProjectEstimates } from "@/hooks/store"; @@ -23,13 +23,16 @@ export const EstimateRoot: FC = (props) => { const { updateProject, currentProjectDetails } = useProject(); const { loader, projectEstimateIds, estimateById, getAllEstimates } = useProjectEstimates(projectId); // states - const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(true); + const [isEstimateCreateModalOpen, setIsEstimateCreateModalOpen] = useState(false); const [isEstimateDeleteModalOpen, setIsEstimateDeleteModalOpen] = useState(null); const [estimateToUpdate, setEstimateToUpdate] = useState(); + console.log("workspaceSlug", workspaceSlug); + console.log("projectId", projectId); + const { isLoading: isSWRLoading } = useSWR( workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => getAllEstimates() : null + async () => workspaceSlug && projectId && getAllEstimates() ); const editEstimate = (estimate: IEstimate) => { @@ -57,32 +60,9 @@ export const EstimateRoot: FC = (props) => { }); }; - if (!workspaceSlug || !projectId) return <>; - return (
- {/* modals */} - {/* create modal */} - { - setIsEstimateCreateModalOpen(false); - setEstimateToUpdate(undefined); - }} - /> - {/* edit modal */} - {/* delete modal */} - setIsEstimateDeleteModalOpen(null)} - data={ - null - // getProjectEstimateById(isEstimateDeleteModalOpen!) - } - /> - - {/* handling the estimates list */} + {}} /> {loader === "init-loader" || isSWRLoading ? ( @@ -140,6 +120,23 @@ export const EstimateRoot: FC = (props) => { )} )} + + { + setIsEstimateCreateModalOpen(false); + setEstimateToUpdate(undefined); + }} + /> + setIsEstimateDeleteModalOpen(null)} + data={ + null + // getProjectEstimateById(isEstimateDeleteModalOpen!) + } + />
); }; diff --git a/web/ee/components/estimates/types.ts b/web/components/estimates/types.ts similarity index 100% rename from web/ee/components/estimates/types.ts rename to web/components/estimates/types.ts diff --git a/web/ee/components/estimates/update-estimate-modal.tsx b/web/components/estimates/update-estimate-modal.tsx similarity index 100% rename from web/ee/components/estimates/update-estimate-modal.tsx rename to web/components/estimates/update-estimate-modal.tsx diff --git a/web/ee/components/estimates/update/index.ts b/web/components/estimates/update/index.ts similarity index 100% rename from web/ee/components/estimates/update/index.ts rename to web/components/estimates/update/index.ts diff --git a/web/ee/components/estimates/update/modal.tsx b/web/components/estimates/update/modal.tsx similarity index 98% rename from web/ee/components/estimates/update/modal.tsx rename to web/components/estimates/update/modal.tsx index e5ef26249..86ee16f34 100644 --- a/web/ee/components/estimates/update/modal.tsx +++ b/web/components/estimates/update/modal.tsx @@ -5,7 +5,7 @@ import { Button } from "@plane/ui"; // components import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; // types -import { TEstimateSystemKeys, TEstimateSystemKeyObject } from "@/ee/components/estimates/types"; +import { TEstimateSystemKeys, TEstimateSystemKeyObject } from "@/components/estimates/types"; type Props = { isOpen: boolean; diff --git a/web/components/radio-group/radio-group.tsx b/web/components/radio-group/radio-group.tsx index b9228009b..949568020 100644 --- a/web/components/radio-group/radio-group.tsx +++ b/web/components/radio-group/radio-group.tsx @@ -37,8 +37,6 @@ const RadioInput = ({ aria = "radio-input"; } - // return

Hello

; - return ( diff --git a/web/constants/estimates.ts b/web/constants/estimates.ts index 0da79fba5..3216d5798 100644 --- a/web/constants/estimates.ts +++ b/web/constants/estimates.ts @@ -1,4 +1,4 @@ -import { TEstimateSystems } from "@/ee/components/estimates/types"; +import { TEstimateSystems } from "@/components/estimates/types"; export const ESTIMATE_SYSTEMS: TEstimateSystems = { points: { diff --git a/web/ee/components/estimates/index.ts b/web/ee/components/estimates/index.ts deleted file mode 100644 index ff3d7f02a..000000000 --- a/web/ee/components/estimates/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./root"; -export * from "./estimate-search"; -export * from "./estimate-disable"; -export * from "./estimate-item"; - -// estimate create -export * from "./create"; - -// estimate update - -// estimate delete diff --git a/web/package.json b/web/package.json index 52542afc4..72aa9d1e0 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.3", + "@headlessui/react": "^2.0.3", "@hello-pangea/dnd": "^16.3.0", "@nivo/bar": "0.80.0", "@nivo/calendar": "0.80.0", @@ -83,4 +83,4 @@ "tsconfig": "*", "typescript": "4.7.4" } -} \ No newline at end of file +} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 5872ed322..a1f414118 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,33 +1,38 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; -// hooks +import { useRouter } from "next/router"; +// components import { PageHead } from "@/components/core"; -import { EstimatesList } from "@/components/estimates"; +import { EstimateRoot } from "@/components/estimates"; import { ProjectSettingHeader } from "@/components/headers"; +// constants import { EUserProjectRoles } from "@/constants/project"; +// hooks import { useUser, useProject } from "@/hooks/store"; // layouts import { AppLayout } from "@/layouts/app-layout"; import { ProjectSettingLayout } from "@/layouts/settings-layout"; -// components // types import { NextPageWithLayout } from "@/lib/types"; -// constants const EstimatesSettingsPage: NextPageWithLayout = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { membership: { currentProjectRole }, } = useUser(); const { currentProjectDetails } = useProject(); + // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; + if (!workspaceSlug || !projectId) return <>; return ( <>
- +
); diff --git a/web/services/project/estimate.service.ts b/web/services/project/estimate.service.ts new file mode 100644 index 000000000..0abc4cbd8 --- /dev/null +++ b/web/services/project/estimate.service.ts @@ -0,0 +1,79 @@ +// types +import { IEstimate } from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +export class EstimateService extends APIService { + constructor() { + super(API_BASE_URL); + } + + // fetching the estimates in workspace level + async fetchWorkspacesList(workspaceSlug: string): Promise { + try { + const { data } = await this.get(`/api/workspaces/${workspaceSlug}/estimates/`); + return data || undefined; + } catch (error) { + throw error; + } + } + + async fetchAll(workspaceSlug: string, projectId: string): Promise { + try { + const { data } = await this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`); + return data || undefined; + } catch (error) { + throw error; + } + } + + async fetchById(workspaceSlug: string, projectId: string, estimateId: string): Promise { + try { + const { data } = await this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` + ); + return data || undefined; + } catch (error) { + throw error; + } + } + + async create(workspaceSlug: string, projectId: string, payload: Partial): Promise { + try { + const { data } = await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, payload); + return data || undefined; + } catch (error) { + throw error; + } + } + + async update( + workspaceSlug: string, + projectId: string, + estimateId: string, + payload: Partial + ): Promise { + try { + const { data } = await this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, + payload + ); + return data || undefined; + } catch (error) { + throw error; + } + } + + async remove(workspaceSlug: string, projectId: string, estimateId: string): Promise { + try { + const { data } = await this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` + ); + return data || undefined; + } catch (error) { + throw error; + } + } +} diff --git a/web/services/project/index.ts b/web/services/project/index.ts index d131ceb6b..10ea7eb9b 100644 --- a/web/services/project/index.ts +++ b/web/services/project/index.ts @@ -1,5 +1,6 @@ export * from "./project.service"; export * from "./project-estimate.service"; +export * from "./estimate.service"; export * from "./project-export.service"; export * from "./project-member.service"; export * from "./project-state.service"; diff --git a/web/store/estimates/project-estimate.store.ts b/web/store/estimates/project-estimate.store.ts index 760dfc59b..f3f153d2c 100644 --- a/web/store/estimates/project-estimate.store.ts +++ b/web/store/estimates/project-estimate.store.ts @@ -25,6 +25,7 @@ export interface IProjectEstimateStore { projectEstimateIds: string[] | undefined; estimateById: (estimateId: string) => IEstimate | undefined; // actions + getWorkspaceAllEstimates: () => Promise; getAllEstimates: () => Promise; getEstimateById: (estimateId: string) => Promise; createEstimate: (data: Partial) => Promise; @@ -48,6 +49,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { // computed projectEstimateIds: computed, // actions + getWorkspaceAllEstimates: action, getAllEstimates: action, getEstimateById: action, createEstimate: action, @@ -79,7 +81,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { * @description fetch all estimates for a project * @returns { IEstimateType[] | undefined } */ - async getWorkspaceAllEstimates(): Promise { + getWorkspaceAllEstimates = async (): Promise => { try { const { workspaceSlug } = this.store.router; if (!workspaceSlug) return; @@ -99,9 +101,9 @@ export class ProjectEstimateStore implements IProjectEstimateStore { message: "Error fetching estimates", }; } - } + }; - async getAllEstimates(): Promise { + getAllEstimates = async (): Promise => { try { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId) return; @@ -121,14 +123,14 @@ export class ProjectEstimateStore implements IProjectEstimateStore { message: "Error fetching estimates", }; } - } + }; /** * @description update an estimate for a project * @param { string } estimateId * @returns IEstimateType | undefined */ - async getEstimateById(estimateId: string): Promise { + getEstimateById = async (estimateId: string): Promise => { try { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId) return; @@ -149,14 +151,14 @@ export class ProjectEstimateStore implements IProjectEstimateStore { message: "Error fetching estimate by id", }; } - } + }; /** * @description create an estimate for a project * @param { Partial } data * @returns */ - async createEstimate(data: Partial): Promise { + createEstimate = async (data: Partial): Promise => { try { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId) return; @@ -174,14 +176,14 @@ export class ProjectEstimateStore implements IProjectEstimateStore { message: "Error creating estimate", }; } - } + }; /** * @description delete an estimate for a project * @param { string } estimateId * @returns void */ - async deleteEstimate(estimateId: string): Promise { + deleteEstimate = async (estimateId: string): Promise => { try { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId) return; @@ -196,5 +198,5 @@ export class ProjectEstimateStore implements IProjectEstimateStore { message: "Error deleting estimate", }; } - } + }; } diff --git a/yarn.lock b/yarn.lock index b1a7ba562..f54a517c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1579,7 +1579,7 @@ dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/react@^0.26.4": +"@floating-ui/react@^0.26.13", "@floating-ui/react@^0.26.4": version "0.26.16" resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.16.tgz#3415a087f452165161c2d313d1d57e8142894679" integrity sha512-HEf43zxZNAI/E781QIVpYSF3K2VH4TTYZpqecjdsFkjsaU1EbaWcM++kw0HXFffj7gDUcBFevX8s0rQGQpxkow== @@ -1593,7 +1593,7 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== -"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.19", "@headlessui/react@^1.7.3": +"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.19": version "1.7.19" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== @@ -1601,6 +1601,16 @@ "@tanstack/react-virtual" "^3.0.0-beta.60" client-only "^0.0.1" +"@headlessui/react@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.0.3.tgz#7506d808a6f3abc307f35354d34b8d642ecfd692" + integrity sha512-Xd1h0YZgfhxZ7W1w4TvK0/TZ1c4qaX4liYVUkAXqW1HCLcXSqnMeYAUGJS/BBroBAUL9HErjyFcRpCWRQZ/0lA== + dependencies: + "@floating-ui/react" "^0.26.13" + "@react-aria/focus" "^3.16.2" + "@react-aria/interactions" "^3.21.1" + "@tanstack/react-virtual" "3.5.0" + "@hello-pangea/dnd@^16.3.0": version "16.6.0" resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" @@ -2207,6 +2217,45 @@ dependencies: "@babel/runtime" "^7.13.10" +"@react-aria/focus@^3.16.2": + version "3.17.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.17.1.tgz#c796a188120421e2fedf438cadacdf463c77ad29" + integrity sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ== + dependencies: + "@react-aria/interactions" "^3.21.3" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + +"@react-aria/interactions@^3.21.1", "@react-aria/interactions@^3.21.3": + version "3.21.3" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.21.3.tgz#a2a3e354a8b894bed7a46e1143453f397f2538d7" + integrity sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + +"@react-aria/ssr@^3.9.4": + version "3.9.4" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.4.tgz#9da8b10342c156e816dbfa4c9e713b21f274d7ab" + integrity sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-aria/utils@^3.24.1": + version "3.24.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.24.1.tgz#9d16023f07c23c41793c9030a9bd203a9c8cf0a7" + integrity sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-stately/utils" "^3.10.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + "@react-spring/animated@~9.4.5": version "9.4.5" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54" @@ -2253,6 +2302,18 @@ "@react-spring/shared" "~9.4.5" "@react-spring/types" "~9.4.5" +"@react-stately/utils@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.1.tgz#dc8685b4994bef0dc10c37b024074be8afbfba62" + integrity sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-types/shared@^3.23.1": + version "3.23.1" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.23.1.tgz#2f23c81d819d0ef376df3cd4c944be4d6bce84c3" + integrity sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw== + "@remirror/core-constants@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" @@ -3390,6 +3451,13 @@ "@swc/counter" "^0.1.3" tslib "^2.4.0" +"@swc/helpers@^0.5.0": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7" + integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A== + dependencies: + tslib "^2.4.0" + "@swc/types@0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.7.tgz#ea5d658cf460abff51507ca8d26e2d391bafb15e" @@ -3407,7 +3475,7 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" -"@tanstack/react-virtual@^3.0.0-beta.60": +"@tanstack/react-virtual@3.5.0", "@tanstack/react-virtual@^3.0.0-beta.60": version "3.5.0" resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.0.tgz#873b5b77cf78af563a4a11e6251ed51ee8868132" integrity sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw== @@ -10177,7 +10245,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -"prettier-fallback@npm:prettier@^3", prettier@^3.1.1, prettier@^3.2.5, prettier@latest: +"prettier-fallback@npm:prettier@^3": version "3.2.5" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== @@ -10204,6 +10272,11 @@ prettier@^2.8.8: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.1.1, prettier@^3.2.5, prettier@latest: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -11607,8 +11680,16 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11704,7 +11785,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13100,8 +13188,16 @@ workbox-window@6.6.1, workbox-window@^6.5.4: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 3133207c42bf1dcd4697de79af5f0b51c6b89e6f Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Thu, 23 May 2024 15:58:14 +0530 Subject: [PATCH 03/59] Move sortabla and radio input to packages/ui --- packages/ui/package.json | 2 +- packages/ui/src/index.ts | 2 ++ packages/ui/src/radio-input/index.tsx | 1 + packages/ui/src/radio-input/radio-input.tsx | 36 ++++++++++++++----- .../ui/src}/sortable/draggable.tsx | 9 ++--- packages/ui/src/sortable/index.ts | 2 ++ packages/ui/src/sortable/sortable.stories.tsx | 32 +++++++++++++++++ .../ui/src}/sortable/sortable.tsx | 0 web/components/estimates/create/stage-one.tsx | 4 +-- web/components/estimates/create/stage-two.tsx | 3 +- web/components/estimates/estimate-item.tsx | 2 +- web/components/radio-group/index.tsx | 1 - yarn.lock | 4 +-- 13 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/radio-input/index.tsx rename {web/components => packages/ui/src}/sortable/draggable.tsx (84%) create mode 100644 packages/ui/src/sortable/index.ts create mode 100644 packages/ui/src/sortable/sortable.stories.tsx rename {web/components => packages/ui/src}/sortable/sortable.tsx (100%) delete mode 100644 web/components/radio-group/index.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 62c335839..ca8275b40 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,7 +22,7 @@ "dependencies": { "@blueprintjs/core": "^4.16.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.0.3", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "emoji-picker-react": "^4.5.16", diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 5402a43e5..b533d3a12 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -15,3 +15,5 @@ export * from "./toast"; export * from "./drag-handle"; export * from "./typography"; export * from "./drop-indicator"; +export * from "./radio-input"; +export * from "./sortable"; diff --git a/packages/ui/src/radio-input/index.tsx b/packages/ui/src/radio-input/index.tsx new file mode 100644 index 000000000..99ac1d9b6 --- /dev/null +++ b/packages/ui/src/radio-input/index.tsx @@ -0,0 +1 @@ +export * from "./radio-input"; diff --git a/packages/ui/src/radio-input/radio-input.tsx b/packages/ui/src/radio-input/radio-input.tsx index abafccc1d..87666a7c1 100644 --- a/packages/ui/src/radio-input/radio-input.tsx +++ b/packages/ui/src/radio-input/radio-input.tsx @@ -1,19 +1,33 @@ -import { Field, Label, Radio, RadioGroup } from "@headlessui/react"; import React from "react"; +import { Field, Label, Radio, RadioGroup } from "@headlessui/react"; +import { cn } from "../../helpers"; +// helpers type RadioInputProps = { label: string | React.ReactNode | undefined; + labelClassName?: string; ariaLabel?: string; options: { label: string; value: string; disabled?: boolean }[]; vertical?: boolean; selected: string; + onChange: (value: string) => void; + className?: string; }; -const RadioInput = ({ label: inputLabel, options, vertical, selected, ariaLabel }: RadioInputProps) => { +const RadioInput = ({ + label: inputLabel, + labelClassName: inputLabelClassName, + options, + vertical, + selected, + ariaLabel, + onChange, + className, +}: RadioInputProps) => { const wrapperClass = vertical ? "flex flex-col gap-1" : "flex gap-2"; const setSelected = (value: string) => { - console.log(value); + onChange(value); }; let aria = ariaLabel ? ariaLabel.toLowerCase().replace(" ", "-") : ""; @@ -23,19 +37,25 @@ const RadioInput = ({ label: inputLabel, options, vertical, selected, ariaLabel aria = "radio-input"; } + // return

Hello

; + return ( - - + +
- {options.map(({ value, label }) => ( + {options.map(({ value, label, disabled }) => ( - + ))}
diff --git a/web/components/sortable/draggable.tsx b/packages/ui/src/sortable/draggable.tsx similarity index 84% rename from web/components/sortable/draggable.tsx rename to packages/ui/src/sortable/draggable.tsx index f4271a848..cdeef1347 100644 --- a/web/components/sortable/draggable.tsx +++ b/packages/ui/src/sortable/draggable.tsx @@ -1,14 +1,15 @@ -import { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { isEqual } from "lodash"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "../../helpers"; type Props = { children: React.ReactNode; data: any; //@todo make this generic + className?: string; }; -const Draggable = ({ children, data }: Props) => { +const Draggable = ({ children, data, className }: Props) => { const ref = useRef(null); const [dragging, setDragging] = useState(false); // NEW const [isDraggedOver, setIsDraggedOver] = useState(false); @@ -37,7 +38,7 @@ const Draggable = ({ children, data }: Props) => { }, [data]); return ( -
+
{children}
); diff --git a/packages/ui/src/sortable/index.ts b/packages/ui/src/sortable/index.ts new file mode 100644 index 000000000..9dde5a404 --- /dev/null +++ b/packages/ui/src/sortable/index.ts @@ -0,0 +1,2 @@ +export * from "./sortable"; +export * from "./draggable"; diff --git a/packages/ui/src/sortable/sortable.stories.tsx b/packages/ui/src/sortable/sortable.stories.tsx new file mode 100644 index 000000000..f5654e8d0 --- /dev/null +++ b/packages/ui/src/sortable/sortable.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import { Draggable } from "./draggable"; +import { Sortable } from "./sortable"; + +const meta: Meta = { + title: "Sortable", + component: Sortable, +}; + +export default meta; +type Story = StoryObj; + +const data = [ + { id: "1", name: "John Doe" }, + { id: "2", name: "Jane Doe" }, + { id: "3", name: "Alice" }, + { id: "4", name: "Bob" }, + { id: "5", name: "Charlie" }, +]; +export const Default: Story = { + args: { + data, + render: (item: any) => ( + +
{item.name}
+
+ ), + onChange: (data) => console.log(data), + keyExtractor: (item: any) => item.id, + }, +}; diff --git a/web/components/sortable/sortable.tsx b/packages/ui/src/sortable/sortable.tsx similarity index 100% rename from web/components/sortable/sortable.tsx rename to packages/ui/src/sortable/sortable.tsx diff --git a/web/components/estimates/create/stage-one.tsx b/web/components/estimates/create/stage-one.tsx index ccd8023cd..0567298c3 100644 --- a/web/components/estimates/create/stage-one.tsx +++ b/web/components/estimates/create/stage-one.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; // components -import { RadioInput } from "@/components/radio-group"; // constants +import { RadioInput } from "@plane/ui"; +import { TEstimateSystemKeys } from "@/components/estimates/types"; import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; // types -import { TEstimateSystemKeys } from "@/components/estimates/types"; type TEstimateCreateStageOne = { estimateSystem: TEstimateSystemKeys; diff --git a/web/components/estimates/create/stage-two.tsx b/web/components/estimates/create/stage-two.tsx index 74a1de0ae..04c50da41 100644 --- a/web/components/estimates/create/stage-two.tsx +++ b/web/components/estimates/create/stage-two.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { Plus } from "lucide-react"; -import { Button } from "@plane/ui"; +import { Button, Sortable } from "@plane/ui"; // components import { EstimateItem } from "@/components/estimates"; import { @@ -10,7 +10,6 @@ import { TEstimateSystemKeyObject, TEstimateSystemKeys, } from "@/components/estimates/types"; -import { Sortable } from "@/components/sortable/sortable"; // constants import { ESTIMATE_SYSTEMS } from "@/constants/estimates"; diff --git a/web/components/estimates/estimate-item.tsx b/web/components/estimates/estimate-item.tsx index aa9e14b31..e881e5d18 100644 --- a/web/components/estimates/estimate-item.tsx +++ b/web/components/estimates/estimate-item.tsx @@ -1,7 +1,7 @@ import { Fragment, useRef, useState } from "react"; import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react"; import { Select } from "@headlessui/react"; -import { Draggable } from "@/components/sortable/draggable"; +import { Draggable } from "@plane/ui"; import { InlineEdit } from "./inline-editable"; import { TEstimatePointNumeric, TEstimatePointString } from "./types"; diff --git a/web/components/radio-group/index.tsx b/web/components/radio-group/index.tsx deleted file mode 100644 index ffd376c16..000000000 --- a/web/components/radio-group/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "./radio-group"; diff --git a/yarn.lock b/yarn.lock index f54a517c2..436886520 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1593,7 +1593,7 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== -"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.19": +"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.19": version "1.7.19" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== @@ -4094,7 +4094,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48": +"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48": version "18.2.48" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== From 51a3e2c63c0c7a01e19a19ed4f455dfa1ed59641 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 23 May 2024 16:53:48 +0530 Subject: [PATCH 04/59] chore: updated empty and loading screens --- web/components/estimates/empty-screen.tsx | 10 ++++++---- web/components/estimates/index.ts | 5 ++++- web/components/estimates/loader-screen.tsx | 9 +++++++++ web/components/estimates/root.tsx | 5 +++-- 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 web/components/estimates/loader-screen.tsx diff --git a/web/components/estimates/empty-screen.tsx b/web/components/estimates/empty-screen.tsx index 03f21ab8f..45dcca8ee 100644 --- a/web/components/estimates/empty-screen.tsx +++ b/web/components/estimates/empty-screen.tsx @@ -17,7 +17,7 @@ export const EstimateEmptyScreen: FC = (props) => { }.webp`; return ( -
+
= (props) => { className="object-contain w-full h-full" />
-
-

No estimate systems yet

-

Explain estimates system here

+
+

No estimate systems yet

+

+ Create a set of estimates to communicate the amount of work per issue. +

diff --git a/web/components/estimates/index.ts b/web/components/estimates/index.ts index 33d533057..deaa289dc 100644 --- a/web/components/estimates/index.ts +++ b/web/components/estimates/index.ts @@ -1,7 +1,10 @@ -export * from "./root"; export * from "./empty-screen"; +export * from "./loader-screen"; + export * from "./estimate-search"; export * from "./estimate-disable"; + +export * from "./root"; export * from "./estimate-item"; // estimate create diff --git a/web/components/estimates/loader-screen.tsx b/web/components/estimates/loader-screen.tsx new file mode 100644 index 000000000..65d6b1897 --- /dev/null +++ b/web/components/estimates/loader-screen.tsx @@ -0,0 +1,9 @@ +import { FC } from "react"; + +export const EstimateLoaderScreen: FC = () => ( +
+ {/* header */} + {/* estimate disable */} +
Loading screen
+
+); diff --git a/web/components/estimates/root.tsx b/web/components/estimates/root.tsx index e808243d7..3abe709f6 100644 --- a/web/components/estimates/root.tsx +++ b/web/components/estimates/root.tsx @@ -4,7 +4,7 @@ import { IEstimate } from "@plane/types"; import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components import { EmptyState } from "@/components/empty-state"; -import { CreateEstimateModal, EstimateEmptyScreen } from "@/components/estimates"; +import { EstimateEmptyScreen, EstimateLoaderScreen, CreateEstimateModal } from "@/components/estimates"; import { DeleteEstimateModal, EstimateListItem } from "@/components/estimates-legacy"; // constants import { EmptyStateType } from "@/constants/empty-state"; @@ -61,7 +61,8 @@ export const EstimateRoot: FC = (props) => { }; return ( -
+
+ {}} /> {loader === "init-loader" || isSWRLoading ? ( From c811ecaa15488891268e2be76df4c6df12d6d439 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 24 May 2024 12:09:36 +0530 Subject: [PATCH 05/59] chore: delete an estimate point --- apiserver/plane/app/urls/estimate.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/estimate/base.py | 104 ++++++++++++------ .../db/migrations/0066_auto_20240522_0957.py | 24 ++++ apiserver/plane/db/models/issue.py | 7 ++ 5 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 apiserver/plane/db/migrations/0066_auto_20240522_0957.py diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index d8571ff0c..d5a87c413 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -4,6 +4,7 @@ from django.urls import path from plane.app.views import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + DeleteEstimatePoint, ) @@ -34,4 +35,9 @@ urlpatterns = [ ), name="bulk-create-estimate-points", ), + path( + "workspaces//projects//estimates//estimate-point//", + DeleteEstimatePoint.as_view({"patch": "partial_update"}), + name="bulk-create-estimate-points", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index bf765e719..07a4e6e9e 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -189,6 +189,7 @@ from .external.base import ( from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + DeleteEstimatePoint, ) from .inbox.base import InboxViewSet, InboxIssueViewSet diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index 7ac3035a9..bfe4e91df 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -1,3 +1,6 @@ +import random +import string + # Third party imports from rest_framework.response import Response from rest_framework import status @@ -5,7 +8,7 @@ from rest_framework import status # Module imports from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission -from plane.db.models import Project, Estimate, EstimatePoint +from plane.db.models import Project, Estimate, EstimatePoint, Issue from plane.app.serializers import ( EstimateSerializer, EstimatePointSerializer, @@ -13,6 +16,12 @@ from plane.app.serializers import ( ) from plane.utils.cache import invalidate_cache + +def generate_random_name(length=10): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) + + class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, @@ -49,13 +58,14 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def create(self, request, slug, project_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate_name = generate_random_name() + estimate = Estimate.objects.create( + name=estimate_name, project_id=project_id + ) estimate_points = request.data.get("estimate_points", []) @@ -67,14 +77,6 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - estimate_serializer = EstimateSerializer( - data=request.data.get("estimate") - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - estimate = estimate_serializer.save(project_id=project_id) estimate_points = EstimatePoint.objects.bulk_create( [ EstimatePoint( @@ -99,7 +101,6 @@ class BulkEstimatePointEndpoint(BaseViewSet): return Response( { - "estimate": estimate_serializer.data, "estimate_points": estimate_point_serializer.data, }, status=status.HTTP_200_OK, @@ -115,13 +116,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def partial_update(self, request, slug, project_id, estimate_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) if not len(request.data.get("estimate_points", [])): return Response( @@ -129,17 +127,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - estimate = Estimate.objects.get(pk=estimate_id) - - estimate_serializer = EstimateSerializer( - estimate, data=request.data.get("estimate"), partial=True - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - estimate = estimate_serializer.save() + _ = Estimate.objects.get(pk=estimate_id) estimate_points_data = request.data.get("estimate_points", []) @@ -178,16 +166,62 @@ class BulkEstimatePointEndpoint(BaseViewSet): ) return Response( { - "estimate": estimate_serializer.data, "estimate_points": estimate_point_serializer.data, }, status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id ) estimate.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class DeleteEstimatePoint(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + def partial_update( + self, request, slug, project_id, estimate_id, estimate_point_id + ): + new_estimate_id = request.data.get("new_estimate_id", None) + estimate_points = EstimatePoint.objects.filter( + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + # update all the issues with the new estimate + if new_estimate_id: + _ = Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + estimate_id=estimate_point_id, + ).update(estimate_id=new_estimate_id) + + # delete the estimate point + old_estimate_point = EstimatePoint.objects.filter( + pk=estimate_point_id + ).first() + + # rearrange the estimate points + updated_estimate_points = [] + for estimate_point in estimate_points: + if estimate_point.key > old_estimate_point.key: + estimate_point.key -= 1 + updated_estimate_points.append(estimate_point) + + EstimatePoint.objects.bulk_update( + updated_estimate_points, + ["key"], + batch_size=10, + ) + + old_estimate_point.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/db/migrations/0066_auto_20240522_0957.py b/apiserver/plane/db/migrations/0066_auto_20240522_0957.py new file mode 100644 index 000000000..6681b6891 --- /dev/null +++ b/apiserver/plane/db/migrations/0066_auto_20240522_0957.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2024-05-22 09:57 + +from django.db import migrations, models +import django.db.models.deletion + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0065_auto_20240415_0937"), + ] + + operations = [ + migrations.AddField( + model_name="issue", + name="estimate", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_estimate", + to="db.estimatepoint", + ), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 7a17853c3..41aacf194 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -124,6 +124,13 @@ class Issue(ProjectBaseModel): null=True, blank=True, ) + estimate = models.ForeignKey( + "db.Estimate", + on_delete=models.SET_NULL, + related_name="issue_estimate", + null=True, + blank=True, + ) name = models.CharField(max_length=255, verbose_name="Issue Name") description = models.JSONField(blank=True, default=dict) description_html = models.TextField(blank=True, default="

") From 8fa2e9b60b61518c850bbda2ff5ddcf931c00899 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 24 May 2024 13:16:41 +0530 Subject: [PATCH 06/59] chore: estimate point response change --- apiserver/plane/app/urls/estimate.py | 2 +- apiserver/plane/app/views/estimate/base.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index d5a87c413..88f1cec9f 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -38,6 +38,6 @@ urlpatterns = [ path( "workspaces//projects//estimates//estimate-point//", DeleteEstimatePoint.as_view({"patch": "partial_update"}), - name="bulk-create-estimate-points", + name="delete-estimate-points", ), ] diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index bfe4e91df..f79aac8c9 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -224,4 +224,7 @@ class DeleteEstimatePoint(BaseViewSet): old_estimate_point.delete() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + EstimatePointSerializer(updated_estimate_points, many=True).data, + status=status.HTTP_200_OK, + ) From 3800f4366cddd714839e864d6589729f6497ad77 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 24 May 2024 15:07:44 +0530 Subject: [PATCH 07/59] chore: updated create estimate and handled the build error --- packages/types/src/estimate.d.ts | 6 +- packages/ui/package.json | 2 +- packages/ui/src/radio-input/radio-input.tsx | 4 +- packages/ui/src/sortable/sortable.tsx | 4 +- web/components/core/activity.tsx | 34 +- .../core/modals/gpt-assistant-popover.tsx | 18 +- web/components/dropdowns/estimate.tsx | 21 +- .../empty-state/comic-box-button.tsx | 14 +- .../create-update-estimate-modal.tsx | 292 -------- .../delete-estimate-modal.tsx | 79 --- .../estimates-legacy/estimate-list-item.tsx | 114 --- .../estimates-legacy/estimates-list.tsx | 121 ---- web/components/estimates-legacy/index.ts | 4 - web/components/estimates/create/modal.tsx | 69 +- web/components/estimates/create/stage-one.tsx | 4 +- web/components/estimates/create/stage-two.tsx | 49 +- web/components/estimates/estimate-item.tsx | 4 +- web/components/estimates/index.ts | 1 + web/components/estimates/inline-editable.tsx | 5 +- web/components/estimates/root.tsx | 55 +- web/components/estimates/types.ts | 18 +- .../estimates/update-estimate-modal.tsx | 207 ------ web/components/estimates/update/modal.tsx | 6 +- .../create-edit-modal/issue-properties.tsx | 6 +- .../activity/actions/estimate.tsx | 11 +- .../issues/issue-detail/sidebar.tsx | 11 +- .../properties/all-properties.tsx | 12 +- web/components/issues/issue-modal/form.tsx | 13 +- web/components/workspace/sidebar-dropdown.tsx | 52 +- web/constants/estimates.ts | 86 +-- .../store/estimates/use-project-estimate.ts | 4 +- web/hooks/use-project-issue-properties.ts | 6 +- web/hooks/use-workspace-issue-properties.ts | 6 +- web/layouts/auth-layout/project-wrapper.tsx | 6 +- web/lib/app-provider.tsx | 8 +- web/next.config.js | 20 +- web/package.json | 10 +- web/services/project/estimate.service.ts | 46 +- web/store/estimate.store.ts | 222 ------ web/store/estimates/estimate-point.ts | 20 +- web/store/estimates/estimate.ts | 53 +- web/store/estimates/project-estimate.store.ts | 178 +++-- .../issue/issue-details/sub_issues.store.ts | 5 +- web/store/root.store.ts | 4 - yarn.lock | 659 +++++++++--------- 45 files changed, 813 insertions(+), 1756 deletions(-) delete mode 100644 web/components/estimates-legacy/create-update-estimate-modal.tsx delete mode 100644 web/components/estimates-legacy/delete-estimate-modal.tsx delete mode 100644 web/components/estimates-legacy/estimate-list-item.tsx delete mode 100644 web/components/estimates-legacy/estimates-list.tsx delete mode 100644 web/components/estimates-legacy/index.ts delete mode 100644 web/components/estimates/update-estimate-modal.tsx delete mode 100644 web/store/estimate.store.ts diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts index c2cd2d766..f25b99c56 100644 --- a/packages/types/src/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -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; }[]; diff --git a/packages/ui/package.json b/packages/ui/package.json index ca8275b40..26696e4b7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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": { diff --git a/packages/ui/src/radio-input/radio-input.tsx b/packages/ui/src/radio-input/radio-input.tsx index 87666a7c1..823bc1079 100644 --- a/packages/ui/src/radio-input/radio-input.tsx +++ b/packages/ui/src/radio-input/radio-input.tsx @@ -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; diff --git a/packages/ui/src/sortable/sortable.tsx b/packages/ui/src/sortable/sortable.tsx index 145908e79..e8d2ae503 100644 --- a/packages/ui/src/sortable/sortable.tsx +++ b/packages/ui/src/sortable/sortable.tsx @@ -21,7 +21,7 @@ const moveItems = (data: T[], source: T, destination: T): T[] => { return newData; }; -const Sortable = ({ data, render, onChange, keyExtractor }: Props) => { +export const Sortable = ({ data, render, onChange, keyExtractor }: Props) => { useEffect(() => { const unsubscribe = monitorForElements({ onDrop({ source, location }) { @@ -46,4 +46,4 @@ const Sortable = ({ data, render, onChange, keyExtractor }: Props) => { ); }; -export { Sortable }; +export default Sortable; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 97927d216..d592db802 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -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 ( - - {areEstimatesEnabledForCurrentProject - ? estimateValue - : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} - - ); -}); +// return ( +// +// {areEstimatesEnabledForCurrentProject +// ? estimateValue +// : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} +// +// ); +// }); const inboxActivityMessage = { declined: { @@ -267,7 +270,8 @@ const activityDetails: { else return ( <> - set the estimate point to + set the estimate point to + {/* */} {showIssue && ( <> {" "} diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index a67b13f7e..9843124e4 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -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 @@ -173,14 +173,14 @@ export const GptAssistantPopover: React.FC = (props) => { const generateResponseButtonText = isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"; + ? "Generate response" + : "Generate again"; return ( - + - + = (props) => { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - } style={styles.popper} {...attributes.popper} > @@ -261,7 +261,7 @@ export const GptAssistantPopover: React.FC = (props) => {
- + ); diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 58243cc22..da0c77d9b 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -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 = 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 = 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({ diff --git a/web/components/empty-state/comic-box-button.tsx b/web/components/empty-state/comic-box-button.tsx index 0bf546a2f..8aeba83eb 100644 --- a/web/components/empty-state/comic-box-button.tsx +++ b/web/components/empty-state/comic-box-button.tsx @@ -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) => { return ( - +
{icon} {label} @@ -55,12 +55,12 @@ export const ComicBoxButton: React.FC = (props) => {
- + {isHovered && ( - } style={styles.popper} {...attributes.popper} static @@ -68,7 +68,7 @@ export const ComicBoxButton: React.FC = (props) => {

{title}

{description}

- + )} ); diff --git a/web/components/estimates-legacy/create-update-estimate-modal.tsx b/web/components/estimates-legacy/create-update-estimate-modal.tsx deleted file mode 100644 index c8b44cc32..000000000 --- a/web/components/estimates-legacy/create-update-estimate-modal.tsx +++ /dev/null @@ -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 = 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({ - 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 ( - -
-
-
{data ? "Update" : "Create"} Estimate
-
-
- ( - - )} - /> -
-
- ( -