From 0d036e6bf582cf4c66a191bfe71919abb2715d42 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:36:55 +0530 Subject: [PATCH 1/9] refactor: dropdown button components (#3508) * refactor: dropdown button components * chore: dropdowns accessibility improvement * chore: update module dropdown * chore: update option content * chore: hide icon from the peek overview --------- Co-authored-by: Anmol Singh Bhatia --- web/components/core/image-picker-popover.tsx | 22 +- web/components/dropdowns/buttons.tsx | 101 +++++ web/components/dropdowns/constants.ts | 20 + web/components/dropdowns/cycle.tsx | 238 +++--------- web/components/dropdowns/date.tsx | 306 +++------------ web/components/dropdowns/estimate.tsx | 255 +++---------- web/components/dropdowns/index.ts | 1 - web/components/dropdowns/member/avatar.tsx | 37 ++ web/components/dropdowns/member/buttons.tsx | 187 ---------- web/components/dropdowns/member/index.ts | 1 - .../dropdowns/member/project-member.tsx | 139 +++---- .../dropdowns/member/workspace-member.tsx | 226 ++++++------ .../dropdowns/module-select/button.tsx | 114 ------ .../dropdowns/module-select/index.ts | 2 - .../dropdowns/module-select/select.tsx | 227 ------------ .../dropdowns/module-select/types.d.ts | 50 --- web/components/dropdowns/module.tsx | 349 +++++++++--------- web/components/dropdowns/priority.tsx | 151 +++----- web/components/dropdowns/project.tsx | 248 +++---------- web/components/dropdowns/state.tsx | 255 +++---------- web/components/dropdowns/types.d.ts | 3 +- web/components/issues/draft-issue-form.tsx | 10 +- .../issues/issue-detail/module-select.tsx | 55 +-- .../issues/issue-detail/parent-select.tsx | 2 +- .../issues/issue-detail/relation-select.tsx | 2 +- web/components/issues/issue-detail/root.tsx | 8 +- .../issues/issue-detail/sidebar.tsx | 6 +- .../properties/all-properties.tsx | 10 +- web/components/issues/issue-modal/form.tsx | 16 +- .../issues/peek-overview/properties.tsx | 6 +- web/components/issues/select/label.tsx | 34 +- web/hooks/use-dropdown-key-down.tsx | 16 +- 32 files changed, 921 insertions(+), 2176 deletions(-) create mode 100644 web/components/dropdowns/buttons.tsx create mode 100644 web/components/dropdowns/constants.ts create mode 100644 web/components/dropdowns/member/avatar.tsx delete mode 100644 web/components/dropdowns/member/buttons.tsx delete mode 100644 web/components/dropdowns/module-select/button.tsx delete mode 100644 web/components/dropdowns/module-select/index.ts delete mode 100644 web/components/dropdowns/module-select/select.tsx delete mode 100644 web/components/dropdowns/module-select/types.d.ts 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; From 3a4c89336834f440887deed74a3da48bcdb60b0c Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:42:20 +0530 Subject: [PATCH 2/9] Style/workspace project settings layout (#3520) * style: project settings layout for mobile screens * style: workspace settings layout for mobile screens --- .../account/o-auth/google-sign-in.tsx | 1 - .../widgets/issue-panels/issues-list.tsx | 2 +- .../widgets/issues-by-state-group.tsx | 26 ++++---- web/components/headers/project-settings.tsx | 65 ++++++++++++------- web/components/headers/workspace-settings.tsx | 24 ++++++- web/components/project/form.tsx | 4 +- web/components/workspace/sidebar-dropdown.tsx | 59 ++++++++++------- .../settings-layout/project/layout.tsx | 8 ++- .../settings-layout/workspace/layout.tsx | 8 ++- 9 files changed, 126 insertions(+), 71 deletions(-) diff --git a/web/components/account/o-auth/google-sign-in.tsx b/web/components/account/o-auth/google-sign-in.tsx index 93958bbd2..c1c57baa0 100644 --- a/web/components/account/o-auth/google-sign-in.tsx +++ b/web/components/account/o-auth/google-sign-in.tsx @@ -31,7 +31,6 @@ export const GoogleSignInButton: FC = (props) => { size: "large", logo_alignment: "center", text: type === "sign_in" ? "signin_with" : "signup_with", - width: 384, } as GsiButtonConfiguration // customization attributes ); } catch (err) { diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index 2076de7fb..af2c11660 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC = (props) => { })} > Issues - + {totalIssues} diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index f8cd2d50a..bd4171cfa 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); @@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) /> {totalCount > 0 ? ( -
-
-
+
+
+
datum.data.color} @@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) layers={["arcs", CenteredMetric]} />
-
+
{chartData.map((item) => (
diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index c15a33f8b..eff05aba5 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -2,13 +2,13 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // ui -import { Breadcrumbs } from "@plane/ui"; +import { Breadcrumbs, CustomMenu } from "@plane/ui"; // helper import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useProject, useUser } from "hooks/store"; // constants -import { EUserProjectRoles } from "constants/project"; +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // components import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -20,7 +20,7 @@ export const ProjectSettingHeader: FC = observer((props) const { title } = props; // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; // store hooks const { membership: { currentProjectRole }, @@ -31,29 +31,48 @@ export const ProjectSettingHeader: FC = observer((props) return (
+
-
- - - {currentProjectDetails?.name.charAt(0)} - - ) - } - link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} - /> - - +
+ + + {currentProjectDetails?.name.charAt(0)} + + ) + } + link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} + /> +
+ +
+
+
+ {title} + } + placement="bottom-start" + closeOnSelect + > + {PROJECT_SETTINGS_LINKS.map((item) => ( + router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}> + {item.label} + + ))} +
); diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx index 0086f17fe..625b7991c 100644 --- a/web/components/headers/workspace-settings.tsx +++ b/web/components/headers/workspace-settings.tsx @@ -1,12 +1,13 @@ import { FC } from "react"; import { useRouter } from "next/router"; // ui -import { Breadcrumbs } from "@plane/ui"; +import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { Settings } from "lucide-react"; // hooks import { observer } from "mobx-react-lite"; // components import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; export interface IWorkspaceSettingHeader { title: string; @@ -21,7 +22,7 @@ export const WorkspaceSettingHeader: FC = observer((pro return (
- +
= observer((pro icon={} link={`/${workspaceSlug}/settings`} /> - +
+ +
+ {title} + } + placement="bottom-start" + closeOnSelect + > + {WORKSPACE_SETTINGS_LINKS.map((item) => ( + router.push(`/${workspaceSlug}${item.href}`)}> + {item.label} + + ))} +
); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 3badbb8bf..f4ab3e846 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -147,10 +147,10 @@ export const ProjectDetailsForm: FC = (props) => { return (
-
+
{watch("cover_image")!} -
+
diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 202eec6b4..0dfcfb803 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -1,4 +1,4 @@ -import { Fragment } from "react"; +import { Fragment, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; @@ -6,6 +6,7 @@ import { useTheme } from "next-themes"; import { Menu, Transition } from "@headlessui/react"; import { mutate } from "swr"; import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; +import { usePopper } from "react-popper"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; // hooks @@ -14,7 +15,6 @@ import useToast from "hooks/use-toast"; import { Avatar, Loader } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; - // Static Data const userLinks = (workspaceSlug: string, userId: string) => [ { @@ -36,7 +36,6 @@ const userLinks = (workspaceSlug: string, userId: string) => [ icon: Settings, }, ]; - const profileLinks = (workspaceSlug: string, userId: string) => [ { name: "View profile", @@ -49,7 +48,6 @@ const profileLinks = (workspaceSlug: string, userId: string) => [ link: "/profile", }, ]; - export const WorkspaceSidebarDropdown = observer(() => { // router const router = useRouter(); @@ -64,12 +62,25 @@ export const WorkspaceSidebarDropdown = observer(() => { // hooks const { setToastAlert } = useToast(); const { setTheme } = useTheme(); - + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "right", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); const handleWorkspaceNavigation = (workspace: IWorkspace) => updateCurrentUser({ last_workspace_id: workspace?.id, }); - const handleSignOut = async () => { await signOut() .then(() => { @@ -85,16 +96,12 @@ export const WorkspaceSidebarDropdown = observer(() => { }) ); }; - const handleItemClick = () => { - console.log('CLICKED') if (window.innerWidth < 768) { toggleSidebar(); } }; - const workspacesList = Object.values(workspaces ?? {}); - // TODO: fix workspaces list scroll return (
@@ -121,14 +128,12 @@ export const WorkspaceSidebarDropdown = observer(() => { activeWorkspace?.name?.charAt(0) ?? "..." )}
- {!sidebarCollapsed && (

{activeWorkspace?.name ? activeWorkspace.name : "Loading..."}

)}
- {!sidebarCollapsed && (
- { workspace?.name?.charAt(0) ?? "..." )} -
{ {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( - { - if (index > 0) handleItemClick(); - }}> + { + if (index > 0) handleItemClick(); + }} + > { )} - {!sidebarCollapsed && ( - + { className="!text-base" /> - {
{currentUser?.email} {profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( - { if (index == 0) handleItemClick(); }}> + { + if (index == 0) handleItemClick(); + }} + > @@ -323,4 +338,4 @@ export const WorkspaceSidebarDropdown = observer(() => { )}
); -}); +}); \ No newline at end of file diff --git a/web/layouts/settings-layout/project/layout.tsx b/web/layouts/settings-layout/project/layout.tsx index c029643bf..f5f131269 100644 --- a/web/layouts/settings-layout/project/layout.tsx +++ b/web/layouts/settings-layout/project/layout.tsx @@ -41,11 +41,13 @@ export const ProjectSettingLayout: FC = observer((props) } /> ) : ( -
-
+
+
- {children} +
+ {children} +
); }); diff --git a/web/layouts/settings-layout/workspace/layout.tsx b/web/layouts/settings-layout/workspace/layout.tsx index fac64bf2e..4ee0f1e33 100644 --- a/web/layouts/settings-layout/workspace/layout.tsx +++ b/web/layouts/settings-layout/workspace/layout.tsx @@ -10,11 +10,13 @@ export const WorkspaceSettingLayout: FC = (props) => { const { children } = props; return ( -
-
+
+
- {children} +
+ {children} +
); }; From 2c67aced155d730d68322f385b8356a0315a4eeb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:09:24 +0530 Subject: [PATCH 3/9] fix: cycle and module sidebar mutation fix (#3521) * fix: cycle and module sidebar mutation * fix: cycle and module calendar drag n drop fix --- web/components/cycles/sidebar.tsx | 18 ++++++++++-------- .../calendar/base-calendar-root.tsx | 3 ++- web/store/issue/cycle/issue.store.ts | 5 ++--- web/store/issue/module/issue.store.ts | 1 + web/store/issue/root.store.ts | 6 ++++++ 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 33c2b3c82..52da51b77 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -587,14 +587,16 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
- -
+ {cycleDetails && cycleDetails.distribution && ( +
+ +
+ )}
) : ( "" diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 3b3ef887e..7cb53ad39 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -61,7 +61,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { projectId?.toString(), issueStore, issueMap, - groupedIssueIds + groupedIssueIds, + viewId ).catch((err) => { setToastAlert({ title: "Error", diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 1a8343006..286222e4a 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -200,6 +200,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { if (!cycleId) throw new Error("Cycle Id is required"); const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); @@ -267,9 +268,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { }); runInAction(() => { - update(this.issues, cycleId, (cycleIssueIds = []) => { - return uniq(concat(cycleIssueIds, issueIds)); - }); + update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds))); }); issueIds.forEach((issueId) => { this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index da2b127c1..e32b97df0 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -205,6 +205,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { if (!moduleId) throw new Error("Module Id is required"); const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index 04f46c280..b2425757c 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -38,6 +38,8 @@ export interface IIssueRootStore { members: string[] | undefined; projects: string[] | undefined; + rootStore: RootStore; + issues: IIssueStore; state: IStateStore; @@ -87,6 +89,8 @@ export class IssueRootStore implements IIssueRootStore { members: string[] | undefined = undefined; projects: string[] | undefined = undefined; + rootStore: RootStore; + issues: IIssueStore; state: IStateStore; @@ -136,6 +140,8 @@ export class IssueRootStore implements IIssueRootStore { projects: observable, }); + this.rootStore = rootStore; + autorun(() => { if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id; if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug; From dc5a5f4a91ac583e83e803daccb2224432e200e6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:47:47 +0530 Subject: [PATCH 4/9] fix/draft issue modal focus issue fix (#3522) --- web/components/issues/draft-issue-form.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 3c9870469..79b91ef40 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -257,14 +257,16 @@ export const DraftIssueForm: FC = observer((props) => { }; useEffect(() => { - setFocus("name"); - reset({ ...defaultValues, ...(prePopulatedData ?? {}), ...(data ?? {}), }); - }, [setFocus, prePopulatedData, reset, data]); + }, [prePopulatedData, reset, data]); + + useEffect(() => { + setFocus("name"); + }, [setFocus]); // update projectId in form when projectId changes useEffect(() => { From 21bc668a569a109c6b4de18c04afbc82f1f4610d Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:05:06 +0530 Subject: [PATCH 5/9] fix: horizontal rule no more causes issues on last node (#3507) --- .../src/ui/extensions/horizontal-rule.tsx | 109 ------------------ .../editor/core/src/ui/extensions/index.tsx | 18 +-- .../core/src/ui/read-only/extensions.tsx | 6 +- 3 files changed, 12 insertions(+), 121 deletions(-) delete mode 100644 packages/editor/core/src/ui/extensions/horizontal-rule.tsx diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule.tsx b/packages/editor/core/src/ui/extensions/horizontal-rule.tsx deleted file mode 100644 index cee0ded83..000000000 --- a/packages/editor/core/src/ui/extensions/horizontal-rule.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { TextSelection } from "prosemirror-state"; - -import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core"; - -/** - * Extension based on: - * - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule) - */ - -export interface HorizontalRuleOptions { - HTMLAttributes: Record; -} - -declare module "@tiptap/core" { - interface Commands { - horizontalRule: { - /** - * Add a horizontal rule - */ - setHorizontalRule: () => ReturnType; - }; - } -} - -export const HorizontalRule = Node.create({ - name: "horizontalRule", - - addOptions() { - return { - HTMLAttributes: {}, - }; - }, - - group: "block", - - addAttributes() { - return { - color: { - default: "#dddddd", - }, - }; - }, - - parseHTML() { - return [ - { - tag: `div[data-type="${this.name}"]`, - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return [ - "div", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - "data-type": this.name, - }), - ["div", {}], - ]; - }, - - addCommands() { - return { - setHorizontalRule: - () => - ({ chain }) => { - return ( - chain() - .insertContent({ type: this.name }) - // set cursor after horizontal rule - .command(({ tr, dispatch }) => { - if (dispatch) { - const { $to } = tr.selection; - const posAfter = $to.end(); - - if ($to.nodeAfter) { - tr.setSelection(TextSelection.create(tr.doc, $to.pos)); - } else { - // add node after horizontal rule if it’s the end of the document - const node = $to.parent.type.contentMatch.defaultType?.create(); - - if (node) { - tr.insert(posAfter, node); - tr.setSelection(TextSelection.create(tr.doc, posAfter)); - } - } - - tr.scrollIntoView(); - } - - return true; - }) - .run() - ); - }, - }; - }, - - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, match }) => { - state.tr.replaceRangeWith(range.from, range.to, this.type.create()); - }, - }), - ]; - }, -}); diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 19d8ce894..5bfba3b0f 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -1,26 +1,25 @@ -import StarterKit from "@tiptap/starter-kit"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; import { Color } from "@tiptap/extension-color"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; +import TextStyle from "@tiptap/extension-text-style"; +import TiptapUnderline from "@tiptap/extension-underline"; +import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; -import { TableHeader } from "src/ui/extensions/table/table-header/table-header"; import { Table } from "src/ui/extensions/table/table"; import { TableCell } from "src/ui/extensions/table/table-cell/table-cell"; +import { TableHeader } from "src/ui/extensions/table/table-header/table-header"; import { TableRow } from "src/ui/extensions/table/table-row/table-row"; -import { HorizontalRule } from "src/ui/extensions/horizontal-rule"; import { ImageExtension } from "src/ui/extensions/image"; import { isValidHttpUrl } from "src/lib/utils"; import { Mentions } from "src/ui/mentions"; -import { CustomKeymap } from "src/ui/extensions/keymap"; import { CustomCodeBlockExtension } from "src/ui/extensions/code"; -import { CustomQuoteExtension } from "src/ui/extensions/quote"; import { ListKeymap } from "src/ui/extensions/custom-list-keymap"; +import { CustomKeymap } from "src/ui/extensions/keymap"; +import { CustomQuoteExtension } from "src/ui/extensions/quote"; import { DeleteImage } from "src/types/delete-image"; import { IMentionSuggestion } from "src/types/mention-suggestion"; @@ -55,7 +54,9 @@ export const CoreEditorExtensions = ( }, code: false, codeBlock: false, - horizontalRule: false, + horizontalRule: { + HTMLAttributes: { class: "mt-4 mb-4" }, + }, blockquote: false, dropcursor: { color: "rgba(var(--color-text-100))", @@ -104,7 +105,6 @@ export const CoreEditorExtensions = ( transformCopiedText: true, transformPastedText: true, }), - HorizontalRule, Table, TableHeader, TableCell, diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index b0879d8cd..cf7c4ee18 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -11,7 +11,6 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header"; import { Table } from "src/ui/extensions/table/table"; import { TableCell } from "src/ui/extensions/table/table-cell/table-cell"; import { TableRow } from "src/ui/extensions/table/table-row/table-row"; -import { HorizontalRule } from "src/ui/extensions/horizontal-rule"; import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image"; import { isValidHttpUrl } from "src/lib/utils"; @@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }, }, codeBlock: false, - horizontalRule: false, + horizontalRule: { + HTMLAttributes: { class: "mt-4 mb-4" }, + }, dropcursor: { color: "rgba(var(--color-text-100))", width: 2, @@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { class: "rounded-lg border border-custom-border-300", }, }), - HorizontalRule, TiptapUnderline, TextStyle, Color, From 70172f8e3dcdf58e31027caad0500c61f94d531a Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:06:12 +0530 Subject: [PATCH 6/9] fix: adding back enter key extension with mentions (#3499) --- packages/editor/core/src/ui/mentions/custom.tsx | 5 +++++ .../editor/core/src/ui/mentions/suggestion.ts | 15 ++++++++++++--- .../src/ui/extensions/enter-key-extension.tsx | 11 +++++++---- .../lite-text-editor/src/ui/extensions/index.tsx | 4 +--- .../issue-activity/comments/comment-create.tsx | 16 ++++++++++++++-- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx index 6a47d79f0..e723ca0d7 100644 --- a/packages/editor/core/src/ui/mentions/custom.tsx +++ b/packages/editor/core/src/ui/mentions/custom.tsx @@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions { } export const CustomMention = Mention.extend({ + addStorage(this) { + return { + mentionsOpen: false, + }; + }, addAttributes() { return { id: { diff --git a/packages/editor/core/src/ui/mentions/suggestion.ts b/packages/editor/core/src/ui/mentions/suggestion.ts index 6d706cb79..40e75a1e3 100644 --- a/packages/editor/core/src/ui/mentions/suggestion.ts +++ b/packages/editor/core/src/ui/mentions/suggestion.ts @@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({ return { onStart: (props: { editor: Editor; clientRect: DOMRect }) => { + props.editor.storage.mentionsOpen = true; reactRenderer = new ReactRenderer(MentionList, { props, editor: props.editor, @@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({ return true; } - // @ts-ignore - return reactRenderer?.ref?.onKeyDown(props); + const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; + + if (navigationKeys.includes(props.event.key)) { + // @ts-ignore + reactRenderer?.ref?.onKeyDown(props); + event?.stopPropagation(); + return true; + } + return false; }, - onExit: () => { + onExit: (props: { editor: Editor; event: KeyboardEvent }) => { + props.editor.storage.mentionsOpen = false; popup?.[0].destroy(); reactRenderer?.destroy(); }, diff --git a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx index 129efa4ee..7d93bf36f 100644 --- a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx +++ b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx @@ -4,13 +4,16 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({ name: "enterKey", - addKeyboardShortcuts() { + addKeyboardShortcuts(this) { return { Enter: () => { - if (onEnterKeyPress) { - onEnterKeyPress(); + if (!this.editor.storage.mentionsOpen) { + if (onEnterKeyPress) { + onEnterKeyPress(); + } + return true; } - return true; + return false; }, "Shift-Enter": ({ editor }) => editor.commands.first(({ commands }) => [ diff --git a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx index 527fd5674..c4b24d166 100644 --- a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx @@ -1,5 +1,3 @@ import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension"; -export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [ - // EnterKeyExtension(onEnterKeyPress), -]; +export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)]; diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index 172a0bb82..bb79c9817 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -10,7 +10,7 @@ import { TActivityOperations } from "../root"; import { TIssueComment } from "@plane/types"; // icons import { Globe2, Lock } from "lucide-react"; -import { useWorkspace } from "hooks/store"; +import { useMention, useWorkspace } from "hooks/store"; const fileService = new FileService(); @@ -43,6 +43,8 @@ export const IssueCommentCreate: FC = (props) => { const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; + const { mentionHighlights, mentionSuggestions } = useMention(); + // refs const editorRef = useRef(null); // react hook form @@ -61,7 +63,14 @@ export const IssueCommentCreate: FC = (props) => { }; return ( -
+
{ + // if (e.key === "Enter" && !e.shiftKey) { + // e.preventDefault(); + // // handleSubmit(onSubmit)(e); + // } + // }} + > = (props) => { render={({ field: { value, onChange } }) => ( { + console.log("yo"); handleSubmit(onSubmit)(e); }} cancelUploadImage={fileService.cancelUpload} @@ -86,6 +96,8 @@ export const IssueCommentCreate: FC = (props) => { onChange={(comment_json: Object, comment_html: string) => { onChange(comment_html); }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} commentAccessSpecifier={ showAccessSpecifier ? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess } From f7803dab561116af5e093360a5ec339e19032730 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:06:57 +0530 Subject: [PATCH 7/9] chore: added validation to cloud hostname field (#3523) --- .../integration/jira/give-details.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/web/components/integration/jira/give-details.tsx b/web/components/integration/jira/give-details.tsx index efeca487a..0d90ba0a5 100644 --- a/web/components/integration/jira/give-details.tsx +++ b/web/components/integration/jira/give-details.tsx @@ -7,6 +7,8 @@ import { Plus } from "lucide-react"; import { useApplication, useProject } from "hooks/store"; // components import { CustomSelect, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; // types import { IJiraImporterForm } from "@plane/types"; @@ -46,17 +48,18 @@ export const JiraGetImportDetail: React.FC = observer(() => { render={({ field: { value, onChange, ref } }) => ( )} /> + {errors.metadata?.api_token &&

{errors.metadata.api_token.message}

}
@@ -75,7 +78,6 @@ export const JiraGetImportDetail: React.FC = observer(() => { render={({ field: { value, onChange, ref } }) => ( { /> )} /> + {errors.metadata?.project_key && ( +

{errors.metadata.project_key.message}

+ )}
@@ -100,11 +105,11 @@ export const JiraGetImportDetail: React.FC = observer(() => { name="metadata.email" rules={{ required: "Please enter email address.", + validate: (value) => checkEmailValidity(value) || "Please enter a valid email address", }} render={({ field: { value, onChange, ref } }) => ( { /> )} /> + {errors.metadata?.email &&

{errors.metadata.email.message}

}
@@ -129,12 +135,11 @@ export const JiraGetImportDetail: React.FC = observer(() => { name="metadata.cloud_hostname" rules={{ required: "Please enter your cloud host name.", + validate: (value) => !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://", }} render={({ field: { value, onChange, ref } }) => ( { /> )} /> + {errors.metadata?.cloud_hostname && ( +

{errors.metadata.cloud_hostname.message}

+ )}
From 6f210e1f4b6b2f55b3fbc542c9f64020dce1425f Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:38:37 +0530 Subject: [PATCH 8/9] chore: removing unnecessary code (#3524) * fix: adding back enter key extension with mentions * removed unncessary code From 4fc4da79826d708051c507ba4e8e4d0c4de38c80 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:16:47 +0530 Subject: [PATCH 9/9] fix: email-template heading overview (#3525) * fix: email-template heading * fix: typo --- .../plane/bgtasks/email_notification_task.py | 2 +- .../emails/notifications/issue-updates.html | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index cc9588ca6..713835033 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -186,7 +186,7 @@ def send_email_notification( } ) - summary = "updates were made to the issue by" + summary = "Updates were made to the issue by" # Send the mail subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}" diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index bdc6a53a3..fa50631c5 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -108,14 +108,33 @@ margin-bottom: 15px; " /> - {% if actors_involved > 0 %} + {% if actors_involved == 1 %} +

+ {{summary}} + + {{ data.0.actor_detail.first_name}} + {{data.0.actor_detail.last_name}} + . +

+ {% else %} +

+ {{summary}} + + {{ data.0.actor_detail.first_name}} + {{data.0.actor_detail.last_name }} + and others. +

+ {% endif %} + + + {% for update in data %} {% if update.changes.name %}