mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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 <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
3ef0570f6a
commit
0d036e6bf5
@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
||||
<Popover.Button
|
||||
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
|
101
web/components/dropdowns/buttons.tsx
Normal file
101
web/components/dropdowns/buttons.tsx
Normal file
@ -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<DropdownButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
|
||||
|
||||
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
|
||||
? BorderButton
|
||||
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
return (
|
||||
<ButtonToRender
|
||||
className={className}
|
||||
isActive={isActive}
|
||||
tooltipContent={tooltipContent}
|
||||
tooltipHeading={tooltipHeading}
|
||||
showTooltip={showTooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonToRender>
|
||||
);
|
||||
};
|
||||
|
||||
const BorderButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
20
web/components/dropdowns/constants.ts
Normal file
20
web/components/dropdowns/constants.ts
Normal file
@ -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",
|
||||
];
|
@ -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 (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}{" "}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
cycle,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
cycle,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@ -153,8 +50,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Cycle",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -221,13 +118,34 @@ export const CycleDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -236,7 +154,7 @@ export const CycleDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -262,77 +180,24 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{/* TODO: move button components to a single file for each dropdown */}
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
cycle={selectedCycle}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
cycle={selectedCycle}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
tooltipHeading="Cycle"
|
||||
tooltipContent={selectedCycle?.name ?? placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedCycle?.name ?? placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -366,7 +231,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -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 (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
clearIconClassName,
|
||||
date,
|
||||
icon,
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
clearIconClassName,
|
||||
date,
|
||||
icon,
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
showPlaceholderIcon = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{showPlaceholderIcon && !date && (
|
||||
<Calendar className="h-2.5 w-2.5 flex-shrink-0 hidden group-hover:inline text-custom-text-400" />
|
||||
)}
|
||||
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateDropdown: React.FC<Props> = (props) => {
|
||||
@ -193,9 +44,8 @@ export const DateDropdown: React.FC<Props> = (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> = (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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -247,90 +118,30 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
date={value}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={value ? renderFormattedDate(value) : "None"}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
|
||||
)}
|
||||
{isClearable && isDateSelected && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(null);
|
||||
}}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
showPlaceholderIcon={showPlaceholderIcon}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
date={value}
|
||||
className={buttonClassName}
|
||||
clearIconClassName={clearIconClassName}
|
||||
hideIcon={hideIcon}
|
||||
icon={icon}
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
showPlaceholderIcon={showPlaceholderIcon}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
@ -338,10 +149,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) closeDropdown();
|
||||
}}
|
||||
onChange={dropdownOnChange}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
|
@ -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 (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
estimatePoint,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
estimatePoint,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Estimate",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -245,7 +143,7 @@ export const EstimateDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
estimatePoint={selectedEstimate}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
estimatePoint={selectedEstimate}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -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";
|
||||
|
37
web/components/dropdowns/member/avatar.tsx
Normal file
37
web/components/dropdowns/member/avatar.tsx
Normal file
@ -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<AvatarProps> = observer((props) => {
|
||||
const { showTooltip, userIds } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
if (Array.isArray(userIds)) {
|
||||
if (userIds.length > 0)
|
||||
return (
|
||||
<AvatarGroup size="md" showTooltip={!showTooltip}>
|
||||
{userIds.map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
|
||||
if (!userDetails) return;
|
||||
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
);
|
||||
} else {
|
||||
if (userIds) {
|
||||
const userDetails = getUserDetails(userIds);
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!showTooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
|
||||
});
|
@ -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 (
|
||||
<AvatarGroup size="md" showTooltip={!tooltip}>
|
||||
{userIds.map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
|
||||
if (!userDetails) return;
|
||||
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
);
|
||||
} else {
|
||||
if (userIds) {
|
||||
const userDetails = getUserDetails(userIds);
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
|
||||
});
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
@ -1,3 +1,2 @@
|
||||
export * from "./buttons";
|
||||
export * from "./project-member";
|
||||
export * from "./workspace-member";
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -112,6 +135,7 @@ export const ProjectMemberDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{Array.isArray(value) && value.length > 0
|
||||
? value.length === 1
|
||||
? getUserDetails(value[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
}}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -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<MemberDropdownProps> = observer((props) => {
|
||||
const {
|
||||
@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = 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<boolean>(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -103,6 +127,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
className={cn("h-full", className)}
|
||||
{...comboboxProps}
|
||||
handleKeyDown={handleKeyDown}
|
||||
onChange={dropdownOnChange}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -125,75 +151,35 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
userIds={value}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
isActive={isOpen}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{Array.isArray(value) && value.length > 0
|
||||
? value.length === 1
|
||||
? getUserDetails(value[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
{isOpen && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||
ref={setPopperElement}
|
||||
@ -222,9 +208,6 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
}}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
@ -243,6 +226,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -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<TModuleSelectButton> = observer((props) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
buttonClassName,
|
||||
buttonVariant,
|
||||
hideIcon,
|
||||
hideText,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
showTooltip,
|
||||
showCount,
|
||||
} = props;
|
||||
// hooks
|
||||
const { getModuleById } = useModule();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
`w-full h-full relative overflow-hidden flex justify-between items-center gap-1 rounded text-sm px-2`,
|
||||
buttonVariant === "border-with-text"
|
||||
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
|
||||
: ``,
|
||||
buttonVariant === "border-without-text"
|
||||
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
|
||||
: ``,
|
||||
buttonVariant === "background-with-text" ? `bg-custom-background-80` : ``,
|
||||
buttonVariant === "background-without-text" ? `bg-custom-background-80` : ``,
|
||||
buttonVariant === "transparent-with-text" ? `hover:bg-custom-background-80` : ``,
|
||||
buttonVariant === "transparent-without-text" ? `hover:bg-custom-background-80` : ``,
|
||||
buttonClassName
|
||||
)}
|
||||
>
|
||||
<div className="relative overflow-hidden h-full flex flex-wrap items-center gap-1">
|
||||
{value && typeof value === "string" ? (
|
||||
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||
{getModuleById(value)?.name || placeholder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : value && Array.isArray(value) && value.length > 0 ? (
|
||||
showCount ? (
|
||||
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||
{value.length} Modules
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
value.map((moduleId) => {
|
||||
const _module = getModuleById(moduleId);
|
||||
if (!_module) return <></>;
|
||||
return (
|
||||
<div className="relative flex justify-between items-center gap-1 min-w-[60px] max-w-[84px] overflow-hidden bg-custom-background-80 px-1.5 py-1 rounded">
|
||||
<Tooltip tooltipContent={_module?.name} disabled={!showTooltip}>
|
||||
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="w-full truncate inline-block line-clamp-1 capitalize">{_module?.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Remove" disabled={!showTooltip}>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onChange(_module.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
!hideText && (
|
||||
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={twMerge("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,2 +0,0 @@
|
||||
export * from "./button";
|
||||
export * from "./select";
|
@ -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<TModuleSelectDropdown> = 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<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(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: (
|
||||
<div className="flex items-center gap-2">
|
||||
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">{moduleDetails?.name}</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
!multiple &&
|
||||
options?.unshift({
|
||||
value: undefined,
|
||||
query: "No module",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">No module</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
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 (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={twMerge("h-full", className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...comboboxProps}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"block h-full max-w-full outline-none",
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"block h-full max-w-full outline-none ",
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<ModuleSelectButton
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonVariant={buttonVariant}
|
||||
hideIcon={hideIcon}
|
||||
hideText={["border-without-text", "background-without-text", "transparent-without-text"].includes(
|
||||
buttonVariant
|
||||
)}
|
||||
dropdownArrow={dropdownArrow}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
showTooltip={showTooltip}
|
||||
showCount={showCount}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => 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";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
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"}`
|
||||
}
|
||||
onClick={() => !multiple && closeDropdown()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
@ -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;
|
||||
};
|
@ -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;
|
||||
showCount?: boolean;
|
||||
} & (
|
||||
| {
|
||||
multiple: false;
|
||||
onChange: (val: string | null) => void;
|
||||
value: string | null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
multiple: true;
|
||||
onChange: (val: string[]) => void;
|
||||
value: string[];
|
||||
}
|
||||
);
|
||||
|
||||
type DropdownOptions =
|
||||
| {
|
||||
@ -32,109 +45,96 @@ 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<ButtonContentProps> = (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();
|
||||
|
||||
if (Array.isArray(value))
|
||||
return (
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{showCount ? (
|
||||
<>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
<span className="flex-grow truncate text-left">
|
||||
{value.length > 0 ? `${value.length} Module${value.length === 1 ? "" : "s"}` : placeholder}
|
||||
</span>
|
||||
</>
|
||||
) : value.length > 0 ? (
|
||||
<div className="flex items-center gap-2 py-0.5 flex-wrap">
|
||||
{value.map((moduleId) => {
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
return (
|
||||
<div
|
||||
key={moduleId}
|
||||
className="flex items-center gap-1 bg-custom-background-80 text-custom-text-200 rounded px-1.5 py-1"
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name}>
|
||||
<span className="text-xs font-medium flex-grow truncate max-w-40">{moduleDetails?.name}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!disabled && (
|
||||
<Tooltip tooltipContent="Remove">
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => {
|
||||
const newModuleIds = value.filter((m) => m !== moduleId);
|
||||
onChange(newModuleIds);
|
||||
}}
|
||||
>
|
||||
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
<span className="flex-grow truncate text-left">{placeholder}</span>
|
||||
</>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
else
|
||||
return (
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{ "bg-custom-background-80": isActive },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{!hideText && <span className="flex-grow truncate text-left">{value ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
|
||||
const options: DropdownOptions = moduleIds?.map((moduleId) => {
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
|
||||
return {
|
||||
value: moduleId,
|
||||
query: `${moduleDetails?.name}`,
|
||||
@ -198,6 +199,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
),
|
||||
};
|
||||
});
|
||||
if (!multiple)
|
||||
options?.unshift({
|
||||
value: null,
|
||||
query: "No module",
|
||||
@ -219,15 +221,41 @@ export const ModuleDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
const comboboxProps: any = {
|
||||
value,
|
||||
onChange: dropdownOnChange,
|
||||
disabled,
|
||||
};
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -235,10 +263,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full", className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...comboboxProps}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
module={selectedModule}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
module={selectedModule}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
tooltipHeading="Module"
|
||||
tooltipContent={
|
||||
Array.isArray(value) ? `${value?.length ?? 0} module${value?.length !== 1 ? "s" : ""}` : ""
|
||||
}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
<ButtonContent
|
||||
disabled={disabled}
|
||||
dropdownArrow={dropdownArrow}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
showCount={showCount}
|
||||
value={value}
|
||||
// @ts-ignore
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
module={selectedModule}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
module={selectedModule}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
module={selectedModule}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
module={selectedModule}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC<Props> = 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 }) => (
|
||||
<>
|
||||
|
@ -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 (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
||||
@ -115,7 +116,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
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 (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
||||
@ -185,7 +186,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
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 (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
@ -260,8 +261,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
highlightUrgent = true,
|
||||
onChange,
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC<Props> = (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<HTMLButtonElement, 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 (
|
||||
<Combobox
|
||||
@ -323,7 +351,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
className
|
||||
)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={dropdownOnChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -349,10 +377,9 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
<ButtonToRender
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
@ -361,74 +388,9 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
showTooltip={showTooltip}
|
||||
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
})}
|
||||
highlightUrgent={highlightUrgent}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
})}
|
||||
highlightUrgent={highlightUrgent}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
})}
|
||||
highlightUrgent={highlightUrgent}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
})}
|
||||
highlightUrgent={highlightUrgent}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
priority={value}
|
||||
className={cn(buttonClassName, {
|
||||
"text-white": resolvedTheme === "dark",
|
||||
})}
|
||||
highlightUrgent={highlightUrgent}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -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 (
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
project,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
project,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange,
|
||||
placeholder = "Project",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -219,7 +128,7 @@ export const ProjectDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
project={selectedProject}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
project={selectedProject}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
project={selectedProject}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
project={selectedProject}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
project={selectedProject}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
project={selectedProject}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Project"
|
||||
tooltipContent={selectedProject?.name ?? placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{selectedProject?.emoji
|
||||
? renderEmoji(selectedProject?.emoji)
|
||||
: selectedProject?.icon_prop
|
||||
? renderEmoji(selectedProject?.icon_prop)
|
||||
: null}
|
||||
</span>
|
||||
)}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedProject?.name ?? placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -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 (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
{
|
||||
"bg-custom-background-80": isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
{
|
||||
"bg-custom-background-80": isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
button,
|
||||
@ -162,8 +41,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange,
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
@ -209,14 +88,35 @@ export const StateDropdown: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -225,7 +125,7 @@ export const StateDropdown: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -251,70 +151,30 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
state={selectedState}
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
tooltipHeading="State"
|
||||
tooltipContent={selectedState?.name ?? "State"}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedState?.group ?? "backlog"}
|
||||
color={selectedState?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
state={selectedState}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
state={selectedState}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
state={selectedState}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
state={selectedState}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
state={selectedState}
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</DropdownButton>
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
@ -348,7 +208,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={closeDropdown}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
3
web/components/dropdowns/types.d.ts
vendored
3
web/components/dropdowns/types.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
CycleDropdown,
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
ModuleSelectDropdown,
|
||||
ModuleDropdown,
|
||||
PriorityDropdown,
|
||||
ProjectDropdown,
|
||||
ProjectMemberDropdown,
|
||||
@ -577,12 +577,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
<ModuleDropdown
|
||||
projectId={projectId}
|
||||
value={value || undefined}
|
||||
onChange={(moduleId) => onChange(moduleId)}
|
||||
value={value ?? []}
|
||||
onChange={onChange}
|
||||
buttonVariant="border-with-text"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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<TIssueModuleSelect> = 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 (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);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(`flex items-center gap-1 h-full`, className)}>
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
<ModuleDropdown
|
||||
projectId={projectId}
|
||||
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleIssueModuleChange}
|
||||
multiple={true}
|
||||
placeholder="No module"
|
||||
disabled={disableSelect}
|
||||
className={`w-full h-full group`}
|
||||
className="w-full h-full group"
|
||||
buttonContainerClassName="w-full"
|
||||
buttonClassName={`min-h-8 ${issue?.module_ids?.length ? `` : `text-custom-text-400`}`}
|
||||
buttonClassName={`min-h-8 text-sm justify-between ${issue?.module_ids?.length ? "" : "text-custom-text-400"}`}
|
||||
buttonVariant="transparent-with-text"
|
||||
hideIcon={false}
|
||||
dropdownArrow={true}
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
showTooltip={true}
|
||||
showTooltip
|
||||
multiple
|
||||
/>
|
||||
|
||||
{isUpdating && <Spinner className="h-4 w-4" />}
|
||||
</div>
|
||||
);
|
||||
|
@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = 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}
|
||||
|
@ -132,7 +132,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = 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}`}
|
||||
|
@ -198,15 +198,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (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.",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -233,7 +233,8 @@ export const IssueDetailsSidebar: React.FC<Props> = 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC<Props> = 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -81,7 +81,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
projectId={issue.project_id}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
tooltip
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@ -95,7 +95,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-without-text"
|
||||
buttonClassName="border"
|
||||
tooltip
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@ -123,7 +123,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
placeholder="Start date"
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={isReadOnly}
|
||||
tooltip
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@ -139,7 +139,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
placeholder="Due date"
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={isReadOnly}
|
||||
tooltip
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@ -169,7 +169,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
projectId={issue.project_id}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
tooltip
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
CycleDropdown,
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
ModuleSelectDropdown,
|
||||
ModuleDropdown,
|
||||
PriorityDropdown,
|
||||
ProjectDropdown,
|
||||
ProjectMemberDropdown,
|
||||
@ -267,6 +267,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
// TODO: update tabIndex logic
|
||||
tabIndex={19}
|
||||
/>
|
||||
</div>
|
||||
@ -547,18 +548,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
<ModuleDropdown
|
||||
projectId={projectId}
|
||||
value={value || undefined}
|
||||
onChange={(moduleId) => {
|
||||
onChange(moduleId);
|
||||
value={value ?? []}
|
||||
onChange={(moduleIds) => {
|
||||
onChange(moduleIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={13}
|
||||
multiple={true}
|
||||
showCount={true}
|
||||
multiple
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC<Props> = 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<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -62,7 +82,7 @@ export const IssueLabelSelect: React.FC<Props> = 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<Props> = 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
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
(onEnterKeyDown: () => void, onEscKeyDown: () => void): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user