diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index a5ffd807a..b2e4c4c9f 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC = observer((props) => { onChange(unsplashImages[0].urls.regular); }, [value, onChange, unsplashImages]); - const openDropdown = () => setIsOpen(true); - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + const handleClose = () => { + if (isOpen) setIsOpen(false); + }; - useOutsideClickDetector(ref, closeDropdown); + const toggleDropdown = () => { + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(ref, handleClose); return ( {label} diff --git a/web/components/dropdowns/buttons.tsx b/web/components/dropdowns/buttons.tsx new file mode 100644 index 000000000..93d8c187c --- /dev/null +++ b/web/components/dropdowns/buttons.tsx @@ -0,0 +1,101 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TButtonVariants } from "./types"; +// constants +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants"; +import { Tooltip } from "@plane/ui"; + +export type DropdownButtonProps = { + children: React.ReactNode; + className?: string; + isActive: boolean; + tooltipContent: string | React.ReactNode; + tooltipHeading: string; + showTooltip: boolean; + variant: TButtonVariants; +}; + +type ButtonProps = { + children: React.ReactNode; + className?: string; + isActive: boolean; + tooltipContent: string | React.ReactNode; + tooltipHeading: string; + showTooltip: boolean; +}; + +export const DropdownButton: React.FC = (props) => { + const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props; + + const ButtonToRender: React.FC = BORDER_BUTTON_VARIANTS.includes(variant) + ? BorderButton + : BACKGROUND_BUTTON_VARIANTS.includes(variant) + ? BackgroundButton + : TransparentButton; + + return ( + + {children} + + ); +}; + +const BorderButton: React.FC = (props) => { + const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props; + + return ( + +
+ {children} +
+
+ ); +}; + +const BackgroundButton: React.FC = (props) => { + const { children, className, tooltipContent, tooltipHeading, showTooltip } = props; + + return ( + +
+ {children} +
+
+ ); +}; + +const TransparentButton: React.FC = (props) => { + const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props; + + return ( + +
+ {children} +
+
+ ); +}; diff --git a/web/components/dropdowns/constants.ts b/web/components/dropdowns/constants.ts new file mode 100644 index 000000000..ce52ad505 --- /dev/null +++ b/web/components/dropdowns/constants.ts @@ -0,0 +1,20 @@ +// types +import { TButtonVariants } from "./types"; + +export const BORDER_BUTTON_VARIANTS: TButtonVariants[] = ["border-with-text", "border-without-text"]; + +export const BACKGROUND_BUTTON_VARIANTS: TButtonVariants[] = ["background-with-text", "background-without-text"]; + +export const TRANSPARENT_BUTTON_VARIANTS: TButtonVariants[] = ["transparent-with-text", "transparent-without-text"]; + +export const BUTTON_VARIANTS_WITHOUT_TEXT: TButtonVariants[] = [ + "border-without-text", + "background-without-text", + "transparent-without-text", +]; + +export const BUTTON_VARIANTS_WITH_TEXT: TButtonVariants[] = [ + "border-with-text", + "background-with-text", + "transparent-with-text", +]; diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index 39b72fe08..d6d4da432 100644 --- a/web/components/dropdowns/cycle.tsx +++ b/web/components/dropdowns/cycle.tsx @@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react"; import { useApplication, useCycle } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { DropdownButton } from "./buttons"; // icons -import { ContrastIcon, Tooltip } from "@plane/ui"; +import { ContrastIcon } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types -import { ICycle } from "@plane/types"; import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -24,18 +27,6 @@ type Props = TDropdownProps & { value: string | null; }; -type ButtonProps = { - className?: string; - cycle: ICycle | null; - hideIcon: boolean; - hideText?: boolean; - dropdownArrow: boolean; - isActive?: boolean; - dropdownArrowClassName: string; - placeholder: string; - tooltip: boolean; -}; - type DropdownOptions = | { value: string | null; @@ -44,100 +35,6 @@ type DropdownOptions = }[] | undefined; -const BorderButton = (props: ButtonProps) => { - const { - className, - cycle, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && }{" "} - {!hideText && {cycle?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - cycle, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && {cycle?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - cycle, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && {cycle?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - export const CycleDropdown: React.FC = observer((props) => { const { button, @@ -153,8 +50,8 @@ export const CycleDropdown: React.FC = observer((props) => { placeholder = "Cycle", placement, projectId, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -221,13 +118,34 @@ export const CycleDropdown: React.FC = observer((props) => { const selectedCycle = value ? getCycleById(value) : null; - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string | null) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { tabIndex={tabIndex} className={cn("h-full", className)} value={value} - onChange={onChange} + onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} > @@ -246,7 +164,7 @@ export const CycleDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -262,77 +180,24 @@ export const CycleDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {/* TODO: move button components to a single file for each dropdown */} - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && } + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + {selectedCycle?.name ?? placeholder} + )} + {dropdownArrow && ( + )} @@ -366,7 +231,6 @@ export const CycleDropdown: React.FC = observer((props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 5dac4ee49..1dba6f780 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -2,17 +2,19 @@ import React, { useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; import DatePicker from "react-datepicker"; import { usePopper } from "react-popper"; -import { Calendar, CalendarDays, X } from "lucide-react"; +import { CalendarDays, X } from "lucide-react"; // hooks import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// ui -import { Tooltip } from "@plane/ui"; +// components +import { DropdownButton } from "./buttons"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { cn } from "helpers/common.helper"; // types import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { clearIconClassName?: string; @@ -23,157 +25,6 @@ type Props = TDropdownProps & { onChange: (val: Date | null) => void; value: Date | string | null; closeOnSelect?: boolean; - showPlaceholderIcon?: boolean; -}; - -type ButtonProps = { - className?: string; - clearIconClassName: string; - date: string | Date | null; - icon: React.ReactNode; - isClearable: boolean; - hideIcon?: boolean; - hideText?: boolean; - isActive?: boolean; - onClear: () => void; - placeholder: string; - tooltip: boolean; - showPlaceholderIcon?: boolean; -}; - -const BorderButton = (props: ButtonProps) => { - const { - className, - clearIconClassName, - date, - icon, - isClearable, - hideIcon = false, - hideText = false, - isActive = false, - onClear, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && icon} - {!hideText && {date ? renderFormattedDate(date) : placeholder}} - {isClearable && ( - { - e.stopPropagation(); - onClear(); - }} - /> - )} -
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - clearIconClassName, - date, - icon, - isClearable, - hideIcon = false, - hideText = false, - onClear, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && icon} - {!hideText && {date ? renderFormattedDate(date) : placeholder}} - {isClearable && ( - { - e.stopPropagation(); - onClear(); - }} - /> - )} -
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - clearIconClassName, - date, - icon, - isClearable, - hideIcon = false, - hideText = false, - isActive = false, - onClear, - placeholder, - tooltip, - showPlaceholderIcon = false, - } = props; - - return ( - -
- {!hideIcon && icon} - {!hideText && {date ? renderFormattedDate(date) : placeholder}} - {showPlaceholderIcon && !date && ( - - )} - - {isClearable && ( - { - e.stopPropagation(); - onClear(); - }} - /> - )} -
-
- ); }; export const DateDropdown: React.FC = (props) => { @@ -193,9 +44,8 @@ export const DateDropdown: React.FC = (props) => { onChange, placeholder = "Date", placement, + showTooltip = false, tabIndex, - tooltip = false, - showPlaceholderIcon = false, value, } = props; const [isOpen, setIsOpen] = useState(false); @@ -217,15 +67,36 @@ export const DateDropdown: React.FC = (props) => { ], }); - const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== ""; + const isDateSelected = value && value.toString().trim() !== ""; - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: Date | null) => { + onChange(val); + if (closeOnSelect) handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = (props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - onChange(null)} - isActive={isOpen} - tooltip={tooltip} - /> - ) : buttonVariant === "border-without-text" ? ( - onChange(null)} - isActive={isOpen} - tooltip={tooltip} - hideText - /> - ) : buttonVariant === "background-with-text" ? ( - onChange(null)} - tooltip={tooltip} - /> - ) : buttonVariant === "background-without-text" ? ( - onChange(null)} - tooltip={tooltip} - hideText - /> - ) : buttonVariant === "transparent-with-text" ? ( - onChange(null)} - isActive={isOpen} - tooltip={tooltip} - showPlaceholderIcon={showPlaceholderIcon} - /> - ) : buttonVariant === "transparent-without-text" ? ( - onChange(null)} - isActive={isOpen} - tooltip={tooltip} - hideText - showPlaceholderIcon={showPlaceholderIcon} - /> - ) : null} + + {!hideIcon && icon} + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + {value ? renderFormattedDate(value) : placeholder} + )} + {isClearable && isDateSelected && ( + { + e.stopPropagation(); + onChange(null); + }} + /> + )} + {isOpen && ( @@ -338,10 +149,7 @@ export const DateDropdown: React.FC = (props) => {
{ - onChange(val); - if (closeOnSelect) closeDropdown(); - }} + onChange={dropdownOnChange} dateFormat="dd-MM-yyyy" minDate={minDate} maxDate={maxDate} diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 31b2a840d..88fec3199 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -8,12 +8,14 @@ import sortBy from "lodash/sortBy"; import { useApplication, useEstimate } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// ui -import { Tooltip } from "@plane/ui"; +// components +import { DropdownButton } from "./buttons"; // helpers import { cn } from "helpers/common.helper"; // types import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -24,18 +26,6 @@ type Props = TDropdownProps & { value: number | null; }; -type ButtonProps = { - className?: string; - estimatePoint: string | null; - dropdownArrow: boolean; - dropdownArrowClassName: string; - hideIcon?: boolean; - hideText?: boolean; - isActive?: boolean; - placeholder: string; - tooltip: boolean; -}; - type DropdownOptions = | { value: number | null; @@ -44,118 +34,6 @@ type DropdownOptions = }[] | undefined; -const BorderButton = (props: ButtonProps) => { - const { - className, - estimatePoint, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && ( - {estimatePoint !== null ? estimatePoint : placeholder} - )} - {dropdownArrow && ( -
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - estimatePoint, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && ( - {estimatePoint !== null ? estimatePoint : placeholder} - )} - {dropdownArrow && ( -
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - estimatePoint, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && ( - {estimatePoint !== null ? estimatePoint : placeholder} - )} - {dropdownArrow && ( -
-
- ); -}; - export const EstimateDropdown: React.FC = observer((props) => { const { button, @@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC = observer((props) => { placeholder = "Estimate", placement, projectId, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC = observer((props) => { const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null; - const openDropdown = () => { - setIsOpen(true); - + const onOpen = () => { if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: number | null) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { tabIndex={tabIndex} className={cn("h-full w-full", className)} value={value} - onChange={onChange} + onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} > @@ -255,7 +153,7 @@ export const EstimateDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && } + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + {selectedEstimate !== null ? selectedEstimate : placeholder} + )} + {dropdownArrow && ( + )} @@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC = observer((props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/index.ts b/web/components/dropdowns/index.ts index 53be7e4f5..036ed9f75 100644 --- a/web/components/dropdowns/index.ts +++ b/web/components/dropdowns/index.ts @@ -3,7 +3,6 @@ export * from "./cycle"; export * from "./date"; export * from "./estimate"; export * from "./module"; -export * from "./module-select"; export * from "./priority"; export * from "./project"; export * from "./state"; diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx new file mode 100644 index 000000000..067d609c5 --- /dev/null +++ b/web/components/dropdowns/member/avatar.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useMember } from "hooks/store"; +// ui +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; + +type AvatarProps = { + showTooltip: boolean; + userIds: string | string[] | null; +}; + +export const ButtonAvatars: React.FC = observer((props) => { + const { showTooltip, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + if (Array.isArray(userIds)) { + if (userIds.length > 0) + return ( + + {userIds.map((userId) => { + const userDetails = getUserDetails(userId); + + if (!userDetails) return; + return ; + })} + + ); + } else { + if (userIds) { + const userDetails = getUserDetails(userIds); + return ; + } + } + + return ; +}); diff --git a/web/components/dropdowns/member/buttons.tsx b/web/components/dropdowns/member/buttons.tsx deleted file mode 100644 index e1664cdb4..000000000 --- a/web/components/dropdowns/member/buttons.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { observer } from "mobx-react-lite"; -import { ChevronDown } from "lucide-react"; -// hooks -import { useMember } from "hooks/store"; -// ui -import { Avatar, AvatarGroup, Tooltip, UserGroupIcon } from "@plane/ui"; -// helpers -import { cn } from "helpers/common.helper"; - -type ButtonProps = { - className?: string; - dropdownArrow: boolean; - dropdownArrowClassName: string; - placeholder: string; - hideIcon?: boolean; - hideText?: boolean; - isActive?: boolean; - tooltip: boolean; - userIds: string | string[] | null; -}; - -const ButtonAvatars = observer(({ tooltip, userIds }: { tooltip: boolean; userIds: string | string[] | null }) => { - const { getUserDetails } = useMember(); - - if (Array.isArray(userIds)) { - if (userIds.length > 0) - return ( - - {userIds.map((userId) => { - const userDetails = getUserDetails(userId); - - if (!userDetails) return; - return ; - })} - - ); - } else { - if (userIds) { - const userDetails = getUserDetails(userIds); - return ; - } - } - - return ; -}); - -export const BorderButton = observer((props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - userIds, - tooltip, - } = props; - // store hooks - const { getUserDetails } = useMember(); - - const isArray = Array.isArray(userIds); - - return ( - -
- {!hideIcon && } - {!hideText && ( - - {isArray && userIds.length > 0 - ? userIds.length === 1 - ? getUserDetails(userIds[0])?.display_name - : "" - : placeholder} - - )} - {dropdownArrow && ( -
-
- ); -}); - -export const BackgroundButton = observer((props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - userIds, - tooltip, - } = props; - // store hooks - const { getUserDetails } = useMember(); - - const isArray = Array.isArray(userIds); - - return ( - -
- {!hideIcon && } - {!hideText && ( - - {isArray && userIds.length > 0 - ? userIds.length === 1 - ? getUserDetails(userIds[0])?.display_name - : "" - : placeholder} - - )} - {dropdownArrow && ( -
-
- ); -}); - -export const TransparentButton = observer((props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - placeholder, - userIds, - tooltip, - } = props; - // store hooks - const { getUserDetails } = useMember(); - - const isArray = Array.isArray(userIds); - - return ( - -
- {!hideIcon && } - {!hideText && ( - - {isArray && userIds.length > 0 - ? userIds.length === 1 - ? getUserDetails(userIds[0])?.display_name - : "" - : placeholder} - - )} - {dropdownArrow && ( -
-
- ); -}); diff --git a/web/components/dropdowns/member/index.ts b/web/components/dropdowns/member/index.ts index bc976b46a..a9f7e09c8 100644 --- a/web/components/dropdowns/member/index.ts +++ b/web/components/dropdowns/member/index.ts @@ -1,3 +1,2 @@ -export * from "./buttons"; export * from "./project-member"; export * from "./workspace-member"; diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx index cc1650527..cfbdf52e6 100644 --- a/web/components/dropdowns/member/project-member.tsx +++ b/web/components/dropdowns/member/project-member.tsx @@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; -import { Check, Search } from "lucide-react"; +import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { useApplication, useMember, useUser } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +import { ButtonAvatars } from "./avatar"; +import { DropdownButton } from "../buttons"; // icons import { Avatar } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types import { MemberDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; type Props = { projectId: string; @@ -36,8 +39,8 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { placeholder = "Members", placement, projectId, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { }; if (multiple) comboboxProps.multiple = true; - const openDropdown = () => { - setIsOpen(true); - + const onOpen = () => { if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string & string[]) => { + onChange(val); + if (!multiple) handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { ref={dropdownRef} tabIndex={tabIndex} className={cn("h-full", className)} + onChange={dropdownOnChange} onKeyDown={handleKeyDown} {...comboboxProps} > @@ -121,7 +145,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && } + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + + {Array.isArray(value) && value.length > 0 + ? value.length === 1 + ? getUserDetails(value[0])?.display_name + : "" + : placeholder} + + )} + {dropdownArrow && ( + )} @@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={() => { - if (!multiple) closeDropdown(); - }} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx index aaa1d7f7e..980f344a6 100644 --- a/web/components/dropdowns/member/workspace-member.tsx +++ b/web/components/dropdowns/member/workspace-member.tsx @@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; -import { Check, Search } from "lucide-react"; +import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { useMember, useUser } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +import { ButtonAvatars } from "./avatar"; +import { DropdownButton } from "../buttons"; // icons import { Avatar } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types import { MemberDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; export const WorkspaceMemberDropdown: React.FC = observer((props) => { const { @@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC = observer(( onChange, placeholder = "Members", placement, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states const [query, setQuery] = useState(""); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); // popper-js refs @@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC = observer(( }; if (multiple) comboboxProps.multiple = true; - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string & string[]) => { + onChange(val); + if (!multiple) handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer(( className={cn("h-full", className)} {...comboboxProps} handleKeyDown={handleKeyDown} + onChange={dropdownOnChange} > {button ? ( @@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC = observer(( ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} + onClick={handleOnClick} > {button} @@ -125,124 +151,82 @@ export const WorkspaceMemberDropdown: React.FC = observer(( }, buttonContainerClassName )} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && } + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + + {Array.isArray(value) && value.length > 0 + ? value.length === 1 + ? getUserDetails(value[0])?.display_name + : "" + : placeholder} + + )} + {dropdownArrow && ( + )} - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={() => { - if (!multiple) closeDropdown(); - }} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + {isOpen && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} +

Loading...

+ )} +
-
- + + )} ); }); diff --git a/web/components/dropdowns/module-select/button.tsx b/web/components/dropdowns/module-select/button.tsx deleted file mode 100644 index 85c97d449..000000000 --- a/web/components/dropdowns/module-select/button.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { FC } from "react"; -import { twMerge } from "tailwind-merge"; -import { observer } from "mobx-react-lite"; -import { ChevronDown, X } from "lucide-react"; -// hooks -import { useModule } from "hooks/store"; -// ui and components -import { DiceIcon, Tooltip } from "@plane/ui"; -// types -import { TModuleSelectButton } from "./types"; - -export const ModuleSelectButton: FC = observer((props) => { - const { - value, - onChange, - placeholder, - buttonClassName, - buttonVariant, - hideIcon, - hideText, - dropdownArrow, - dropdownArrowClassName, - showTooltip, - showCount, - } = props; - // hooks - const { getModuleById } = useModule(); - - return ( -
-
- {value && typeof value === "string" ? ( -
- {!hideIcon && } - {!hideText && ( - - {getModuleById(value)?.name || placeholder} - - )} -
- ) : value && Array.isArray(value) && value.length > 0 ? ( - showCount ? ( -
- {!hideIcon && } - {!hideText && ( - - {value.length} Modules - - )} -
- ) : ( - value.map((moduleId) => { - const _module = getModuleById(moduleId); - if (!_module) return <>; - return ( -
- -
- {!hideIcon && } - {!hideText && ( - {_module?.name} - )} -
-
- - { - e.preventDefault(); - e.stopPropagation(); - onChange(_module.id); - }} - > - - - -
- ); - }) - ) - ) : ( - !hideText && ( -
- {!hideIcon && } - {!hideText && ( - - {placeholder} - - )} -
- ) - )} -
- - {dropdownArrow && ( -
- ); -}); diff --git a/web/components/dropdowns/module-select/index.ts b/web/components/dropdowns/module-select/index.ts deleted file mode 100644 index 2161534fb..000000000 --- a/web/components/dropdowns/module-select/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./button"; -export * from "./select"; diff --git a/web/components/dropdowns/module-select/select.tsx b/web/components/dropdowns/module-select/select.tsx deleted file mode 100644 index a8ddfccf7..000000000 --- a/web/components/dropdowns/module-select/select.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { FC, useEffect, useRef, useState, Fragment } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, Search } from "lucide-react"; -import { twMerge } from "tailwind-merge"; -// hooks -import { useModule } from "hooks/store"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components -import { ModuleSelectButton } from "./"; -// types -import { TModuleSelectDropdown, TModuleSelectDropdownOption } from "./types"; -import { DiceIcon } from "@plane/ui"; - -export const ModuleSelectDropdown: FC = observer((props) => { - // props - const { - workspaceSlug, - projectId, - value = undefined, - onChange, - placeholder = "Module", - multiple = false, - disabled = false, - className = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonVariant = "transparent-with-text", - hideIcon = false, - dropdownArrow = false, - dropdownArrowClassName = "", - showTooltip = false, - showCount = false, - placement, - tabIndex, - button, - } = props; - // states - const [query, setQuery] = useState(""); - const [isOpen, setIsOpen] = useState(false); - // refs - const dropdownRef = useRef(null); - // popper-js refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // store hooks - const { getProjectModuleIds, fetchModules, getModuleById } = useModule(); - - const moduleIds = getProjectModuleIds(projectId); - - const options: TModuleSelectDropdownOption[] | undefined = moduleIds?.map((moduleId) => { - const moduleDetails = getModuleById(moduleId); - return { - value: moduleId, - query: `${moduleDetails?.name}`, - content: ( -
- - {moduleDetails?.name} -
- ), - }; - }); - !multiple && - options?.unshift({ - value: undefined, - query: "No module", - content: ( -
- - No module -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - - // fetch modules of the project if not already present in the store - useEffect(() => { - if (!workspaceSlug) return; - - if (!moduleIds) fetchModules(workspaceSlug, projectId); - }, [moduleIds, fetchModules, projectId, workspaceSlug]); - - const openDropdown = () => { - if (isOpen) closeDropdown(); - else { - setIsOpen(true); - if (referenceElement) referenceElement.focus(); - } - }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); - - const comboboxProps: any = {}; - if (multiple) comboboxProps.multiple = true; - - return ( - - - {button ? ( - - ) : ( - - )} - - {isOpen && ( - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(moduleIds: any) => { - const displayValueOptions: TModuleSelectDropdownOption[] | undefined = options?.filter((_module) => - moduleIds.includes(_module.value) - ); - return displayValueOptions?.map((_option) => _option.query).join(", ") || "Select Module"; - }} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={() => !multiple && closeDropdown()} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} -
-
-
- )} -
- ); -}); diff --git a/web/components/dropdowns/module-select/types.d.ts b/web/components/dropdowns/module-select/types.d.ts deleted file mode 100644 index b1c10eedb..000000000 --- a/web/components/dropdowns/module-select/types.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ReactNode } from "react"; -import { Placement } from "@popperjs/core"; -import { TDropdownProps, TButtonVariants } from "../types"; - -type TModuleSelectDropdownRoot = Omit< - TDropdownProps, - "buttonClassName", - "buttonContainerClassName", - "buttonContainerClassName", - "className", - "disabled", - "hideIcon", - "placeholder", - "placement", - "tabIndex", - "tooltip" ->; - -export type TModuleSelectDropdownBase = { - value: string | string[] | undefined; - onChange: (moduleIds: undefined | string | (string | undefined)[]) => void; - placeholder?: string; - disabled?: boolean; - buttonClassName?: string; - buttonVariant?: TButtonVariants; - hideIcon?: boolean; - dropdownArrow?: boolean; - dropdownArrowClassName?: string; - showTooltip?: boolean; - showCount?: boolean; -}; - -export type TModuleSelectButton = TModuleSelectDropdownBase & { hideText?: boolean }; - -export type TModuleSelectDropdown = TModuleSelectDropdownBase & { - workspaceSlug: string; - projectId: string; - multiple?: boolean; - className?: string; - buttonContainerClassName?: string; - placement?: Placement; - tabIndex?: number; - button?: ReactNode; -}; - -export type TModuleSelectDropdownOption = { - value: string | undefined; - query: string; - content: JSX.Element; -}; diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx index 4fc5e0e22..e673293e0 100644 --- a/web/components/dropdowns/module.tsx +++ b/web/components/dropdowns/module.tsx @@ -2,27 +2,40 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; +import { Check, ChevronDown, Search, X } from "lucide-react"; // hooks import { useApplication, useModule } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { DropdownButton } from "./buttons"; // icons import { DiceIcon, Tooltip } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types -import { IModule } from "@plane/types"; import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; dropdownArrow?: boolean; dropdownArrowClassName?: string; - onChange: (val: string | null) => void; projectId: string; - value: string | null; -}; + showCount?: boolean; +} & ( + | { + multiple: false; + onChange: (val: string | null) => void; + value: string | null; + } + | { + multiple: true; + onChange: (val: string[]) => void; + value: string[]; + } + ); type DropdownOptions = | { @@ -32,110 +45,97 @@ type DropdownOptions = }[] | undefined; -type ButtonProps = { - className?: string; +type ButtonContentProps = { + disabled: boolean; dropdownArrow: boolean; dropdownArrowClassName: string; - hideIcon?: boolean; - hideText?: boolean; - isActive?: boolean; - module: IModule | null; + hideIcon: boolean; + hideText: boolean; + onChange: (moduleIds: string[]) => void; placeholder: string; - tooltip: boolean; + showCount: boolean; + value: string | string[] | null; }; -const BorderButton = (props: ButtonProps) => { +const ButtonContent: React.FC = (props) => { const { - className, + disabled, dropdownArrow, dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - module, + hideIcon, + hideText, + onChange, placeholder, - tooltip, + showCount, + value, } = props; + // store hooks + const { getModuleById } = useModule(); - return ( - -
+ {showCount ? ( + <> + {!hideIcon && } + + {value.length > 0 ? `${value.length} Module${value.length === 1 ? "" : "s"}` : placeholder} + + + ) : value.length > 0 ? ( +
+ {value.map((moduleId) => { + const moduleDetails = getModuleById(moduleId); + return ( +
+ {!hideIcon && } + {!hideText && ( + + {moduleDetails?.name} + + )} + {!disabled && ( + + + + )} +
+ ); + })} +
+ ) : ( + <> + {!hideIcon && } + {placeholder} + )} - > - {!hideIcon && } - {!hideText && {module?.name ?? placeholder}} {dropdownArrow && (
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - module, - placeholder, - tooltip, - } = props; - - return ( - -
+ + ); + else + return ( + <> {!hideIcon && } - {!hideText && {module?.name ?? placeholder}} + {!hideText && {value ?? placeholder}} {dropdownArrow && (
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - module, - placeholder, - tooltip, - } = props; - - return ( - -
- {!hideIcon && } - {!hideText && {module?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); + + ); }; export const ModuleDropdown: React.FC = observer((props) => { @@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC = observer((props) => { dropdownArrow = false, dropdownArrowClassName = "", hideIcon = false, + multiple, onChange, placeholder = "Module", placement, projectId, + showCount = false, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -186,7 +188,6 @@ export const ModuleDropdown: React.FC = observer((props) => { const options: DropdownOptions = moduleIds?.map((moduleId) => { const moduleDetails = getModuleById(moduleId); - return { value: moduleId, query: `${moduleDetails?.name}`, @@ -198,16 +199,17 @@ export const ModuleDropdown: React.FC = observer((props) => { ), }; }); - options?.unshift({ - value: null, - query: "No module", - content: ( -
- - No module -
- ), - }); + if (!multiple) + options?.unshift({ + value: null, + query: "No module", + content: ( +
+ + No module +
+ ), + }); const filteredOptions = query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); @@ -219,15 +221,41 @@ export const ModuleDropdown: React.FC = observer((props) => { if (!moduleIds) fetchModules(workspaceSlug, projectId); }, [moduleIds, fetchModules, projectId, workspaceSlug]); - const selectedModule = value ? getModuleById(value) : null; - - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string & string[]) => { + onChange(val); + if (!multiple) handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); + + const comboboxProps: any = { + value, + onChange: dropdownOnChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; return ( = observer((props) => { ref={dropdownRef} tabIndex={tabIndex} className={cn("h-full", className)} - value={value} - onChange={onChange} - disabled={disabled} onKeyDown={handleKeyDown} + {...comboboxProps} > {button ? ( @@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - + - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + )} @@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC = observer((props) => { key={option.value} value={option.value} className={({ active, selected }) => - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + cn( + "w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none", + { + "bg-custom-background-80": active, + "text-custom-text-100": selected, + "text-custom-text-200": !selected, + } + ) } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 7f96a3bd1..f620e6818 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -15,6 +15,7 @@ import { TIssuePriorities } from "@plane/types"; import { TDropdownProps } from "./types"; // constants import { ISSUE_PRIORITIES } from "constants/issue"; +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -34,7 +35,7 @@ type ButtonProps = { isActive?: boolean; highlightUrgent: boolean; priority: TIssuePriorities; - tooltip: boolean; + showTooltip: boolean; }; const BorderButton = (props: ButtonProps) => { @@ -46,7 +47,7 @@ const BorderButton = (props: ButtonProps) => { hideText = false, highlightUrgent, priority, - tooltip, + showTooltip, } = props; const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); @@ -60,7 +61,7 @@ const BorderButton = (props: ButtonProps) => { }; return ( - +
{ hideText = false, highlightUrgent, priority, - tooltip, + showTooltip, } = props; const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); @@ -129,7 +130,7 @@ const BackgroundButton = (props: ButtonProps) => { }; return ( - +
{ isActive = false, highlightUrgent, priority, - tooltip, + showTooltip, } = props; const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); @@ -199,7 +200,7 @@ const TransparentButton = (props: ButtonProps) => { }; return ( - +
= (props) => { highlightUrgent = true, onChange, placement, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC = (props) => { const filteredOptions = query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: TIssuePriorities) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); + + const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) + ? BorderButton + : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant) + ? BackgroundButton + : TransparentButton; return ( = (props) => { className )} value={value} - onChange={onChange} + onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} > @@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC = (props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -349,86 +377,20 @@ export const PriorityDropdown: React.FC = (props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + )} @@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC = (props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index cdbe21b08..719308a65 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -7,14 +7,15 @@ import { Check, ChevronDown, Search } from "lucide-react"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// ui -import { Tooltip } from "@plane/ui"; +// components +import { DropdownButton } from "./buttons"; // helpers import { cn } from "helpers/common.helper"; import { renderEmoji } from "helpers/emoji.helper"; // types -import { IProject } from "@plane/types"; import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -24,119 +25,6 @@ type Props = TDropdownProps & { value: string | null; }; -type ButtonProps = { - className?: string; - dropdownArrow: boolean; - dropdownArrowClassName: string; - hideIcon?: boolean; - hideText?: boolean; - placeholder: string; - project: IProject | null; - tooltip: boolean; -}; - -const BorderButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - project, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} - - )} - {!hideText && {project?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - project, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} - - )} - {!hideText && {project?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - placeholder, - project, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} - - )} - {!hideText && {project?.name ?? placeholder}} - {dropdownArrow && ( -
-
- ); -}; - export const ProjectDropdown: React.FC = observer((props) => { const { button, @@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC = observer((props) => { onChange, placeholder = "Project", placement, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC = observer((props) => { const selectedProject = value ? getProjectById(value) : null; - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { tabIndex={tabIndex} className={cn("h-full", className)} value={value} - onChange={onChange} + onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} > @@ -229,7 +138,7 @@ export const ProjectDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && ( + + {selectedProject?.emoji + ? renderEmoji(selectedProject?.emoji) + : selectedProject?.icon_prop + ? renderEmoji(selectedProject?.icon_prop) + : null} + + )} + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + {selectedProject?.name ?? placeholder} + )} + {dropdownArrow && ( + )} @@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC = observer((props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index 9ad41622b..e92dd40a0 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react"; import { useApplication, useProjectState } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { DropdownButton } from "./buttons"; // icons -import { StateGroupIcon, Tooltip } from "@plane/ui"; +import { StateGroupIcon } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types -import { IState } from "@plane/types"; import { TDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -24,130 +27,6 @@ type Props = TDropdownProps & { value: string; }; -type ButtonProps = { - className?: string; - dropdownArrow: boolean; - dropdownArrowClassName: string; - hideIcon?: boolean; - hideText?: boolean; - isActive?: boolean; - state: IState | undefined; - tooltip: boolean; -}; - -const BorderButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - state, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - )} - {!hideText && {state?.name ?? "State"}} - {dropdownArrow && ( -
-
- ); -}; - -const BackgroundButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - state, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - )} - {!hideText && {state?.name ?? "State"}} - {dropdownArrow && ( -
-
- ); -}; - -const TransparentButton = (props: ButtonProps) => { - const { - className, - dropdownArrow, - dropdownArrowClassName, - hideIcon = false, - hideText = false, - isActive = false, - state, - tooltip, - } = props; - - return ( - -
- {!hideIcon && ( - - )} - {!hideText && {state?.name ?? "State"}} - {dropdownArrow && ( -
-
- ); -}; - export const StateDropdown: React.FC = observer((props) => { const { button, @@ -162,8 +41,8 @@ export const StateDropdown: React.FC = observer((props) => { onChange, placement, projectId, + showTooltip = false, tabIndex, - tooltip = false, value, } = props; // states @@ -209,14 +88,35 @@ export const StateDropdown: React.FC = observer((props) => { const selectedState = getStateById(value); - const openDropdown = () => { - setIsOpen(true); + const onOpen = () => { if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isOpen) setIsOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { tabIndex={tabIndex} className={cn("h-full", className)} value={value} - onChange={onChange} + onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} > @@ -235,7 +135,7 @@ export const StateDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} - onClick={openDropdown} + onClick={handleOnClick} > {button} @@ -251,70 +151,30 @@ export const StateDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} - onClick={openDropdown} + onClick={handleOnClick} > - {buttonVariant === "border-with-text" ? ( - - ) : buttonVariant === "border-without-text" ? ( - - ) : buttonVariant === "background-with-text" ? ( - - ) : buttonVariant === "background-without-text" ? ( - - ) : buttonVariant === "transparent-with-text" ? ( - - ) : buttonVariant === "transparent-without-text" ? ( - - ) : null} + + {!hideIcon && ( + + )} + {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( + {selectedState?.name ?? "State"} + )} + {dropdownArrow && ( + )} @@ -348,7 +208,6 @@ export const StateDropdown: React.FC = observer((props) => { active ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } - onClick={closeDropdown} > {({ selected }) => ( <> diff --git a/web/components/dropdowns/types.d.ts b/web/components/dropdowns/types.d.ts index b99369022..128c7a525 100644 --- a/web/components/dropdowns/types.d.ts +++ b/web/components/dropdowns/types.d.ts @@ -17,7 +17,6 @@ export type TDropdownProps = { hideIcon?: boolean; placeholder?: string; placement?: Placement; + showTooltip?: boolean; tabIndex?: number; - // TODO: rename this prop to showTooltip - tooltip?: boolean; }; diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index a5b0ef149..3c9870469 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -21,7 +21,7 @@ import { CycleDropdown, DateDropdown, EstimateDropdown, - ModuleSelectDropdown, + ModuleDropdown, PriorityDropdown, ProjectDropdown, ProjectMemberDropdown, @@ -577,12 +577,12 @@ export const DraftIssueForm: FC = observer((props) => { name="module_ids" render={({ field: { value, onChange } }) => (
- onChange(moduleId)} + value={value ?? []} + onChange={onChange} buttonVariant="border-with-text" + multiple />
)} diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index 82ff4ed32..1c4d80168 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -1,10 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -import xor from "lodash/xor"; // hooks import { useIssueDetail } from "hooks/store"; // components -import { ModuleSelectDropdown } from "components/dropdowns"; +import { ModuleDropdown } from "components/dropdowns"; // ui import { Spinner } from "@plane/ui"; // helpers @@ -33,56 +32,42 @@ export const IssueModuleSelect: React.FC = observer((props) const issue = getIssueById(issueId); const disableSelect = disabled || isUpdating; - const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => { - if (!issue) return; + const handleIssueModuleChange = async (moduleIds: string[]) => { + if (!issue || !issue.module_ids) return; setIsUpdating(true); - if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0) - await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue?.module_ids); - if (typeof moduleIds === "string" && moduleIds) - await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]); - - if (Array.isArray(moduleIds)) { - if (moduleIds.includes(undefined)) { - await issueOperations.removeModulesFromIssue?.( - workspaceSlug, - projectId, - issueId, - moduleIds.filter((x) => x != undefined) as string[] - ); - } else { - const _moduleIds = xor(issue?.module_ids, moduleIds)[0]; - if (_moduleIds) { - if (issue?.module_ids?.includes(_moduleIds)) - await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]); - else await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]); - } - } + if (moduleIds.length === 0) + await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids); + else if (moduleIds.length > issue.module_ids.length) { + const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m)); + await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds); + } else if (moduleIds.length < issue.module_ids.length) { + const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m)); + await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds); } + setIsUpdating(false); }; return (
- - {isUpdating && }
); diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx index 7ad8c836f..9a1aa48ad 100644 --- a/web/components/issues/issue-detail/parent-select.tsx +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC = observer((props) href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`} target="_blank" rel="noopener noreferrer" - className="text-xs font-medium mt-0.5" + className="text-xs font-medium" onClick={(e) => e.stopPropagation()} > {parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id} diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 1fdd353a6..67bba8697 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -132,7 +132,7 @@ export const IssueRelationSelect: React.FC = observer((pro href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`} target="_blank" rel="noopener noreferrer" - className="text-xs font-medium mt-0.5" + className="text-xs font-medium" onClick={(e) => e.stopPropagation()} > {`${projectDetails?.identifier}-${currentIssue?.sequence_id}`} diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 9a16dcbf0..fda73e94f 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -198,15 +198,15 @@ export const IssueDetailRoot: FC = (props) => { try { await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); setToastAlert({ - title: "Module removed from issue successfully", type: "success", - message: "Module removed from issue successfully", + title: "Successful!", + message: "Issue removed from module successfully.", }); } catch (error) { setToastAlert({ - title: "Module remove from issue failed", type: "error", - message: "Module remove from issue failed", + title: "Error!", + message: "Issue could not be removed from module. Please try again.", }); } }, diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index f2ee876b9..668d3538f 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -233,7 +233,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon />
@@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon />
diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index c23938a19..4057c7b93 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -81,7 +81,7 @@ export const IssueProperties: React.FC = observer((props) => { projectId={issue.project_id} disabled={isReadOnly} buttonVariant="border-with-text" - tooltip + showTooltip />
@@ -95,7 +95,7 @@ export const IssueProperties: React.FC = observer((props) => { disabled={isReadOnly} buttonVariant="border-without-text" buttonClassName="border" - tooltip + showTooltip />
@@ -123,7 +123,7 @@ export const IssueProperties: React.FC = observer((props) => { placeholder="Start date" buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} disabled={isReadOnly} - tooltip + showTooltip />
@@ -139,7 +139,7 @@ export const IssueProperties: React.FC = observer((props) => { placeholder="Due date" buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} disabled={isReadOnly} - tooltip + showTooltip /> @@ -169,7 +169,7 @@ export const IssueProperties: React.FC = observer((props) => { projectId={issue.project_id} disabled={isReadOnly} buttonVariant="border-with-text" - tooltip + showTooltip /> diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index f1f5d873f..703a9783f 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -20,7 +20,7 @@ import { CycleDropdown, DateDropdown, EstimateDropdown, - ModuleSelectDropdown, + ModuleDropdown, PriorityDropdown, ProjectDropdown, ProjectMemberDropdown, @@ -267,6 +267,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" + // TODO: update tabIndex logic tabIndex={19} /> @@ -547,18 +548,17 @@ export const IssueFormRoot: FC = observer((props) => { name="module_ids" render={({ field: { value, onChange } }) => (
- { - onChange(moduleId); + value={value ?? []} + onChange={(moduleIds) => { + onChange(moduleIds); handleFormChange(); }} buttonVariant="border-with-text" tabIndex={13} - multiple={true} - showCount={true} + multiple + showCount />
)} diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index ea00b845a..7a241e070 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC = observer((pro buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon /> @@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC = observer((pro buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon /> diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 54931f85d..73fddfa9d 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC = observer((props) => { const filteredOptions = query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); - const openDropdown = () => { - setIsDropdownOpen(true); + const onOpen = () => { if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsDropdownOpen(false); - const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen); - useOutsideClickDetector(dropdownRef, closeDropdown); + + const handleClose = () => { + if (isDropdownOpen) setIsDropdownOpen(false); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isDropdownOpen) onOpen(); + setIsDropdownOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string[]) => { + onChange(val); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); return ( = observer((props) => { ref={dropdownRef} tabIndex={tabIndex} value={value} - onChange={(val) => onChange(val)} + onChange={dropdownOnChange} className="relative flex-shrink-0 h-full" multiple disabled={disabled} @@ -73,7 +93,7 @@ export const IssueLabelSelect: React.FC = observer((props) => { type="button" ref={setReferenceElement} className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200" - onClick={openDropdown} + onClick={handleOnClick} > {label ? ( label diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx index 1bb861477..99511b0fc 100644 --- a/web/hooks/use-dropdown-key-down.tsx +++ b/web/hooks/use-dropdown-key-down.tsx @@ -1,23 +1,23 @@ import { useCallback } from "react"; type TUseDropdownKeyDown = { - (onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent) => void; + (onEnterKeyDown: () => void, onEscKeyDown: () => void): (event: React.KeyboardEvent) => void; }; -export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => { +export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => { const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === "Enter") { event.stopPropagation(); - if (!isOpen) { - onOpen(); - } - } else if (event.key === "Escape" && isOpen) { + event.preventDefault(); + onEnterKeyDown(); + } else if (event.key === "Escape") { event.stopPropagation(); - onClose(); + event.preventDefault(); + onEscKeyDown(); } }, - [isOpen, onOpen, onClose] + [onEnterKeyDown, onEscKeyDown] ); return handleKeyDown;