diff --git a/packages/ui/src/radio-input/radio-input.tsx b/packages/ui/src/radio-input/radio-input.tsx index 823bc10790..16e1d5e9cb 100644 --- a/packages/ui/src/radio-input/radio-input.tsx +++ b/packages/ui/src/radio-input/radio-input.tsx @@ -5,9 +5,12 @@ import { cn } from "../../helpers"; type RadioInputProps = { label: string | React.ReactNode | undefined; + wrapperClassName?: string; + fieldClassName?: string; + buttonClassName?: string; labelClassName?: string; ariaLabel?: string; - options: { label: string; value: string; disabled?: boolean }[]; + options: { label: string | React.ReactNode; value: string; disabled?: boolean }[]; vertical?: boolean; selected: string; onChange: (value: string) => void; @@ -16,7 +19,10 @@ type RadioInputProps = { export const RadioInput = ({ label: inputLabel, - labelClassName: inputLabelClassName, + labelClassName: inputLabelClassName = "", + wrapperClassName: inputWrapperClassName = "", + fieldClassName: inputFieldClassName = "", + buttonClassName: inputButtonClassName = "", options, vertical, selected, @@ -42,15 +48,15 @@ export const RadioInput = ({ return ( -
- {options.map(({ value, label, disabled }) => ( - +
+ {options.map(({ value, label, disabled }, index) => ( + diff --git a/web/components/estimates/create/index.ts b/web/components/estimates/create/index.ts index 6b9fd2095f..7de8cb9889 100644 --- a/web/components/estimates/create/index.ts +++ b/web/components/estimates/create/index.ts @@ -1,3 +1,2 @@ export * from "./modal"; export * from "./stage-one"; -export * from "./stage-two"; diff --git a/web/components/estimates/create/modal.tsx b/web/components/estimates/create/modal.tsx index 30cf02ab9d..5ef3ee7de3 100644 --- a/web/components/estimates/create/modal.tsx +++ b/web/components/estimates/create/modal.tsx @@ -5,7 +5,7 @@ import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject } from "@ import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; -import { EstimateCreateStageOne, EstimateCreateStageTwo } from "@/components/estimates"; +import { EstimateCreateStageOne, EstimatePointCreateRoot } from "@/components/estimates"; // constants import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/constants/estimates"; // hooks @@ -36,9 +36,6 @@ export const CreateEstimateModal: FC = observer((props) => } }, [isOpen]); - // derived values - const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); - const handleCreateEstimate = async () => { try { if (!workspaceSlug || !projectId) return; @@ -90,6 +87,9 @@ export const CreateEstimateModal: FC = observer((props) => } }; + // derived values + const renderEstimateStepsCount = useMemo(() => (estimatePoints ? "2" : "1"), [estimatePoints]); + return (
@@ -107,7 +107,7 @@ export const CreateEstimateModal: FC = observer((props) =>
)} -
New Estimate System
+
New Estimate System
Step {renderEstimateStepsCount}/2
@@ -124,12 +124,10 @@ export const CreateEstimateModal: FC = observer((props) => /> )} {estimatePoints && ( - )} diff --git a/web/components/estimates/create/stage-one.tsx b/web/components/estimates/create/stage-one.tsx index afb4aeca40..98e7c423e8 100644 --- a/web/components/estimates/create/stage-one.tsx +++ b/web/components/estimates/create/stage-one.tsx @@ -18,49 +18,51 @@ export const EstimateCreateStageOne: FC = (props) => { if (!currentEstimateSystem) return <>; return ( -
+
{ const currentSystem = system as TEstimateSystemKeys; return { - label: ESTIMATE_SYSTEMS[currentSystem]?.name, + label: ESTIMATE_SYSTEMS[currentSystem]?.name ||
Hello
, value: system, disabled: !ESTIMATE_SYSTEMS[currentSystem]?.is_available, }; })} label="Choose an estimate system" labelClassName="text-sm font-medium text-custom-text-200 mb-3" + wrapperClassName="relative flex flex-wrap gap-14" + fieldClassName="relative flex items-center gap-2" + buttonClassName="size-4" selected={estimateSystem} onChange={(value) => handleEstimateSystem(value as TEstimateSystemKeys)} - className="mb-4" />
-
+
Start from scratch
-
+
Choose a template
{Object.keys(currentEstimateSystem.templates).map((name) => currentEstimateSystem.templates[name]?.hide ? null : ( - )} -
-
- ); -}); diff --git a/web/components/estimates/points/create/index.ts b/web/components/estimates/points/create/index.ts new file mode 100644 index 0000000000..078b530630 --- /dev/null +++ b/web/components/estimates/points/create/index.ts @@ -0,0 +1,3 @@ +export * from "./root"; +export * from "./preview"; +export * from "./update"; diff --git a/web/components/estimates/points/create/preview.tsx b/web/components/estimates/points/create/preview.tsx new file mode 100644 index 0000000000..313ea306ed --- /dev/null +++ b/web/components/estimates/points/create/preview.tsx @@ -0,0 +1,65 @@ +import { FC, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { GripVertical, Pencil, Trash2 } from "lucide-react"; +import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +// components +import { EstimatePointItemCreateUpdate } from "@/components/estimates/points"; + +type TEstimatePointItemCreatePreview = { + estimateType: TEstimateSystemKeys; + estimatePoint: TEstimatePointsObject; + handleEstimatePoint: (mode: "add" | "remove" | "update", value: TEstimatePointsObject) => void; +}; + +export const EstimatePointItemCreatePreview: FC = observer((props) => { + const { estimateType, estimatePoint, handleEstimatePoint } = props; + // state + const [estimatePointEditToggle, setEstimatePointEditToggle] = useState(false); + // ref + const EstimatePointValueRef = useRef(null); + + useEffect(() => { + if (!estimatePointEditToggle) + EstimatePointValueRef?.current?.addEventListener("dblclick", () => setEstimatePointEditToggle(true)); + }, [estimatePointEditToggle]); + + return ( +
+ {!estimatePointEditToggle && ( +
+
+ +
+
+ {estimatePoint?.value ? ( + estimatePoint?.value + ) : ( + Enter Estimate Value + )} +
+
setEstimatePointEditToggle(true)} + > + +
+
handleEstimatePoint("remove", estimatePoint)} + > + +
+
+ )} + + {estimatePoint && estimatePointEditToggle && ( + handleEstimatePoint("update", { ...estimatePoint, value })} + callback={() => setEstimatePointEditToggle(false)} + /> + )} +
+ ); +}); diff --git a/web/components/estimates/points/create/root.tsx b/web/components/estimates/points/create/root.tsx new file mode 100644 index 0000000000..7943743e53 --- /dev/null +++ b/web/components/estimates/points/create/root.tsx @@ -0,0 +1,90 @@ +import { Dispatch, FC, SetStateAction, useCallback } from "react"; +import { observer } from "mobx-react"; +import { Plus } from "lucide-react"; +import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { Button, Draggable, Sortable } from "@plane/ui"; +// components +import { EstimatePointItemCreatePreview } from "@/components/estimates/points"; +// constants +import { maxEstimatesCount } from "@/constants/estimates"; + +type TEstimatePointCreateRoot = { + estimateType: TEstimateSystemKeys; + estimatePoints: TEstimatePointsObject[]; + setEstimatePoints: Dispatch>; +}; + +export const EstimatePointCreateRoot: FC = observer((props) => { + // props + const { estimateType, estimatePoints, setEstimatePoints } = props; + + const handleEstimatePoint = useCallback( + (mode: "add" | "remove" | "update", value: TEstimatePointsObject) => { + switch (mode) { + case "add": + setEstimatePoints((prevValue) => { + prevValue = prevValue ? [...prevValue] : []; + return [...prevValue, value]; + }); + break; + case "update": + setEstimatePoints((prevValue) => { + prevValue = prevValue ? [...prevValue] : []; + return prevValue.map((item) => (item.key === value.key ? { ...item, value: value.value } : item)); + }); + break; + case "remove": + setEstimatePoints((prevValue) => { + prevValue = prevValue ? [...prevValue] : []; + return prevValue.filter((item) => item.key !== value.key); + }); + break; + default: + break; + } + }, + [setEstimatePoints] + ); + + const handleDragEstimatePoints = (updatedEstimatedOrder: TEstimatePointsObject[]) => { + const updatedEstimateKeysOrder = updatedEstimatedOrder.map((item, index) => ({ ...item, key: index + 1 })); + setEstimatePoints(updatedEstimateKeysOrder); + }; + + return ( +
+
{estimateType}
+ ( + + + + )} + onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} + keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} + /> + + {estimatePoints && estimatePoints.length <= maxEstimatesCount && ( + + )} +
+ ); +}); diff --git a/web/components/estimates/points/create/update.tsx b/web/components/estimates/points/create/update.tsx new file mode 100644 index 0000000000..59f50904e8 --- /dev/null +++ b/web/components/estimates/points/create/update.tsx @@ -0,0 +1,107 @@ +import { FC, useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { Check, Info, X } from "lucide-react"; +import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types"; +import { Tooltip } from "@plane/ui"; +// constants +import { EEstimateSystem } from "@/constants/estimates"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type TEstimatePointItemCreateUpdate = { + estimateType: TEstimateSystemKeys; + estimatePoint: TEstimatePointsObject; + updateEstimateValue: (value: string) => void; + callback: () => void; +}; + +export const EstimatePointItemCreateUpdate: FC = observer((props) => { + const { estimateType, estimatePoint, updateEstimateValue, callback } = props; + // states + const [estimateInputValue, setEstimateInputValue] = useState(undefined); + const [error, setError] = useState(undefined); + + useEffect(() => { + if (estimateInputValue === undefined && estimatePoint) setEstimateInputValue(estimatePoint?.value || ""); + }, [estimateInputValue, estimatePoint]); + + const handleClose = () => { + setEstimateInputValue(""); + callback(); + }; + + const handleCreate = async () => { + if (!estimatePoint) return; + if (estimateInputValue) + try { + setError(undefined); + + const currentEstimateType: EEstimateSystem | undefined = estimateType; + let isEstimateValid = false; + + if (currentEstimateType && [(EEstimateSystem.TIME, EEstimateSystem.POINTS)].includes(currentEstimateType)) { + if (estimateInputValue && Number(estimateInputValue) && Number(estimateInputValue) >= 0) { + isEstimateValid = true; + } + } else if (currentEstimateType && currentEstimateType === EEstimateSystem.CATEGORIES) { + if (estimateInputValue && estimateInputValue.length > 0) { + isEstimateValid = true; + } + } + + if (isEstimateValid) { + updateEstimateValue(estimateInputValue); + setError(undefined); + handleClose(); + } else { + setError("please enter a valid estimate value"); + } + } catch { + setError("something went wrong. please try again later"); + } + else { + setError("Please fill the input field"); + } + }; + + return ( +
+
+ setEstimateInputValue(e.target.value)} + className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent" + placeholder="Enter estimate value" + autoFocus + /> + {error && ( + <> + +
+ +
+
+ + )} +
+
+ +
+
+ +
+
+ ); +}); diff --git a/web/components/estimates/points/estimate-point-item.tsx b/web/components/estimates/points/estimate-point-item.tsx deleted file mode 100644 index 5b622c8a86..0000000000 --- a/web/components/estimates/points/estimate-point-item.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { FC, Fragment, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { Check, GripVertical, MoveRight, Pencil, Trash2, X } from "lucide-react"; -import { Select } from "@headlessui/react"; -import { TEstimatePointsObject } from "@plane/types"; -import { Draggable, Spinner } from "@plane/ui"; -// constants -import { EEstimateUpdateStages } from "@/constants/estimates"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { useEstimate, useEstimatePoint } from "@/hooks/store"; - -type TEstimatePointItem = { - workspaceSlug: string; - projectId: string; - estimateId: string | undefined; - mode: EEstimateUpdateStages; - item: TEstimatePointsObject; - editItem: (value: string) => void; - replaceEstimateItem: (value: TEstimatePointsObject) => void; - deleteItem: () => void; -}; - -export const EstimatePointItem: FC = observer((props) => { - // props - const { workspaceSlug, projectId, estimateId, mode, item, editItem, replaceEstimateItem, deleteItem } = props; - const { id, key, value } = item; - // hooks - const { asJson: estimate, creteEstimatePoint, deleteEstimatePoint } = useEstimate(estimateId); - const { updateEstimatePoint } = useEstimatePoint(estimateId, id); - // ref - const inputRef = useRef(null); - // states - const [inputValue, setInputValue] = useState(undefined); - // handling editing states - const [estimateEditLoader, setEstimateEditLoader] = useState(false); - const [deletedEstimateValue, setDeletedEstimateValue] = useState(undefined); - const [isEstimateEditing, setIsEstimateEditing] = useState(false); - const [isEstimateDeleting, setIsEstimateDeleting] = useState(false); - - useEffect(() => { - if (value && inputValue === undefined) setInputValue(value); - }, [value, inputValue]); - - const handleCreateEdit = (value: string) => { - setInputValue(value); - editItem(value); - }; - - const handleNewEstimatePoint = async () => { - if (inputValue) { - try { - setEstimateEditLoader(true); - const estimatePoint = await creteEstimatePoint(workspaceSlug, projectId, { key: key, value: inputValue }); - if (estimatePoint && estimatePoint.key && estimatePoint.value) { - replaceEstimateItem({ id: estimatePoint.id, key: estimatePoint.key, value: estimatePoint.value }); - } - setIsEstimateEditing(false); - setEstimateEditLoader(false); - } catch (error) { - setEstimateEditLoader(false); - } - } - }; - - const handleEdit = async () => { - if (id) { - try { - setEstimateEditLoader(true); - const estimatePoint = await updateEstimatePoint(workspaceSlug, projectId, { - key: key, - value: inputValue || "", - }); - if (estimatePoint) if (estimatePoint) editItem(inputValue || ""); - setIsEstimateEditing(false); - setEstimateEditLoader(false); - } catch (error) { - setEstimateEditLoader(false); - } - } else { - if (inputValue) editItem(inputValue); - } - }; - - const handleDelete = async () => { - if (id) { - try { - setEstimateEditLoader(true); - await deleteEstimatePoint(workspaceSlug, projectId, id, deletedEstimateValue); - deleteItem(); - setIsEstimateDeleting(false); - setEstimateEditLoader(false); - } catch (error) { - setEstimateEditLoader(false); - } - } else { - deleteItem(); - } - }; - - const selectDropdownOptions = estimate && estimate?.points ? estimate?.points.filter((point) => point.id !== id) : []; - - return ( - - {!id && ( - <> - {mode === EEstimateUpdateStages.CREATE && ( -
-
- -
- handleCreateEdit(e.target.value)} - className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none py-2.5 w-full" - /> -
- -
-
- )} - - {mode === EEstimateUpdateStages.EDIT && ( -
-
- setInputValue(e.target.value)} - className={cn( - "border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full", - isEstimateDeleting ? `bg-custom-background-90` : `bg-transparent` - )} - disabled={isEstimateDeleting} - /> -
- {estimateEditLoader ? ( -
- -
- ) : ( -
- -
- )} - -
- -
-
- )} - - )} - - {id && ( - <> - {mode === EEstimateUpdateStages.EDIT && ( - <> - {!isEstimateEditing && !isEstimateDeleting && ( -
-
- -
-
setIsEstimateEditing(true)}> - {value} -
-
setIsEstimateEditing(true)} - > - -
-
setIsEstimateDeleting(true)} - > - -
-
- )} - - {(isEstimateEditing || isEstimateDeleting) && ( -
-
-
- setInputValue(e.target.value)} - className={cn( - "border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full", - isEstimateDeleting ? `bg-custom-background-90` : `bg-transparent` - )} - disabled={isEstimateDeleting} - /> -
- {isEstimateDeleting && ( -
- Mark as -
- )} - {isEstimateDeleting && ( -
- -
- )} -
- {estimateEditLoader ? ( -
- -
- ) : ( -
(isEstimateEditing ? handleEdit() : handleDelete())} - > - {isEstimateEditing ? : } -
- )} - -
(isEstimateEditing ? setIsEstimateEditing(false) : setIsEstimateDeleting(false))} - > - -
-
- )} - - )} - - {mode === EEstimateUpdateStages.SWITCH && ( -
-
- setInputValue(e.target.value)} - className="flex-grow border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 bg-custom-background-90 w-full" - disabled - /> -
- -
- setInputValue(e.target.value)} - className="flex-grow border-none bg-transparent focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full" - /> -
-
- )} - - )} -
- ); -}); diff --git a/web/components/estimates/points/index.ts b/web/components/estimates/points/index.ts index d55a873062..9d5564c163 100644 --- a/web/components/estimates/points/index.ts +++ b/web/components/estimates/points/index.ts @@ -1,5 +1,3 @@ -export * from "./estimate-point-item"; -export * from "./inline-editable"; - +export * from "./create"; export * from "./edit"; export * from "./switch"; diff --git a/web/components/estimates/points/inline-editable.tsx b/web/components/estimates/points/inline-editable.tsx deleted file mode 100644 index e5847f6d83..0000000000 --- a/web/components/estimates/points/inline-editable.tsx +++ /dev/null @@ -1,65 +0,0 @@ -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; -}; - -export 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 - )} -
- ); -};