Merge branch 'develop' of https://github.com/makeplane/plane into fix/gantt-scroll

This commit is contained in:
Aaryan Khandelwal 2024-02-01 13:15:43 +05:30
commit 0cea96060e
57 changed files with 1163 additions and 2407 deletions

View File

@ -186,7 +186,7 @@ def send_email_notification(
} }
) )
summary = "updates were made to the issue by" summary = "Updates were made to the issue by"
# Send the mail # Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}" subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"

View File

@ -108,14 +108,33 @@
margin-bottom: 15px; margin-bottom: 15px;
" "
/> />
{% if actors_involved > 0 %} {% if actors_involved == 1 %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
{{summary}}
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
{{ data.0.actor_detail.first_name}}
{{data.0.actor_detail.last_name}}
</span>.
</p>
{% else %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
{{summary}}
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
{{ data.0.actor_detail.first_name}}
{{data.0.actor_detail.last_name }}
</span>and others.
</p>
{% endif %}
<!-- {% if actors_involved == 1 %}
{% if data|length > 0 and comments|length == 0 %} {% if data|length > 0 and comments|length == 0 %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
<span style="font-size: 1rem; font-weight: 700; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px">
{{ data.0.actor_detail.first_name}} {{ data.0.actor_detail.first_name}}
{{data.0.actor_detail.last_name }} {{data.0.actor_detail.last_name }}
</span> </span>
made {{data|length}} {% if data|length > 1 %}updates{% else %}update{% endif %} to the issue. made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} to the issue.
</p> </p>
{% elif data|length == 0 and comments|length > 0 %} {% elif data|length == 0 and comments|length > 0 %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
@ -123,7 +142,7 @@
{{ comments.0.actor_detail.first_name}} {{ comments.0.actor_detail.first_name}}
{{comments.0.actor_detail.last_name }} {{comments.0.actor_detail.last_name }}
</span> </span>
added {{comments|length}} new {% if comments|length > 1 %}comments{% else %}comment{% endif %}. added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %}.
</p> </p>
{% elif data|length > 0 and comments|length > 0 %} {% elif data|length > 0 and comments|length > 0 %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
@ -131,14 +150,14 @@
{{ data.0.actor_detail.first_name}} {{ data.0.actor_detail.first_name}}
{{data.0.actor_detail.last_name }} {{data.0.actor_detail.last_name }}
</span> </span>
made {{data|length}} {% if data|length > 1 %}updates{% else %}update{% endif %} and added {{comments|length}} new {% if comments|length > 1 %}comments{% else %}comment{% endif %} on the issue. made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} and added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %} on the issue.
</p> </p>
{% endif %} {% endif %}
{% else %} {% else %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
There are {{ data|length }} new updates and {{comments|length}} new comments on the issue There are {{ total_updates }} new updates and {{total_comments}} new comments on the issue.
</p> </p>
{% endif %} {% endif %} -->
{% for update in data %} {% if update.changes.name %} {% for update in data %} {% if update.changes.name %}
<!-- Issue title updated --> <!-- Issue title updated -->
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> <p style="font-size: 1rem; line-height: 28px; color: #1f2d5c">

View File

@ -1,109 +0,0 @@
import { TextSelection } from "prosemirror-state";
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
/**
* Extension based on:
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
*/
export interface HorizontalRuleOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
horizontalRule: {
/**
* Add a horizontal rule
*/
setHorizontalRule: () => ReturnType;
};
}
}
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
name: "horizontalRule",
addOptions() {
return {
HTMLAttributes: {},
};
},
group: "block",
addAttributes() {
return {
color: {
default: "#dddddd",
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
["div", {}],
];
},
addCommands() {
return {
setHorizontalRule:
() =>
({ chain }) => {
return (
chain()
.insertContent({ type: this.name })
// set cursor after horizontal rule
.command(({ tr, dispatch }) => {
if (dispatch) {
const { $to } = tr.selection;
const posAfter = $to.end();
if ($to.nodeAfter) {
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
} else {
// add node after horizontal rule if its the end of the document
const node = $to.parent.type.contentMatch.defaultType?.create();
if (node) {
tr.insert(posAfter, node);
tr.setSelection(TextSelection.create(tr.doc, posAfter));
}
}
tr.scrollIntoView();
}
return true;
})
.run()
);
},
};
},
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, match }) => {
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
},
}),
];
},
});

View File

@ -1,26 +1,25 @@
import StarterKit from "@tiptap/starter-kit";
import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
import TaskItem from "@tiptap/extension-task-item"; import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list"; import TaskList from "@tiptap/extension-task-list";
import TextStyle from "@tiptap/extension-text-style";
import TiptapUnderline from "@tiptap/extension-underline";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown"; import { Markdown } from "tiptap-markdown";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table"; import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell"; import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { TableRow } from "src/ui/extensions/table/table-row/table-row"; import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ImageExtension } from "src/ui/extensions/image"; import { ImageExtension } from "src/ui/extensions/image";
import { isValidHttpUrl } from "src/lib/utils"; import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions"; import { Mentions } from "src/ui/mentions";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomCodeBlockExtension } from "src/ui/extensions/code"; import { CustomCodeBlockExtension } from "src/ui/extensions/code";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { ListKeymap } from "src/ui/extensions/custom-list-keymap"; import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { DeleteImage } from "src/types/delete-image"; import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion"; import { IMentionSuggestion } from "src/types/mention-suggestion";
@ -55,7 +54,9 @@ export const CoreEditorExtensions = (
}, },
code: false, code: false,
codeBlock: false, codeBlock: false,
horizontalRule: false, horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
blockquote: false, blockquote: false,
dropcursor: { dropcursor: {
color: "rgba(var(--color-text-100))", color: "rgba(var(--color-text-100))",
@ -104,7 +105,6 @@ export const CoreEditorExtensions = (
transformCopiedText: true, transformCopiedText: true,
transformPastedText: true, transformPastedText: true,
}), }),
HorizontalRule,
Table, Table,
TableHeader, TableHeader,
TableCell, TableCell,

View File

@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions {
} }
export const CustomMention = Mention.extend<CustomMentionOptions>({ export const CustomMention = Mention.extend<CustomMentionOptions>({
addStorage(this) {
return {
mentionsOpen: false,
};
},
addAttributes() { addAttributes() {
return { return {
id: { id: {

View File

@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return { return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => { onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, { reactRenderer = new ReactRenderer(MentionList, {
props, props,
editor: props.editor, editor: props.editor,
@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return true; return true;
} }
// @ts-ignore const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
return reactRenderer?.ref?.onKeyDown(props);
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
reactRenderer?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
}, },
onExit: () => { onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy(); popup?.[0].destroy();
reactRenderer?.destroy(); reactRenderer?.destroy();
}, },

View File

@ -11,7 +11,6 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table"; import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell"; import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row"; import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image"; import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
import { isValidHttpUrl } from "src/lib/utils"; import { isValidHttpUrl } from "src/lib/utils";
@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}, },
}, },
codeBlock: false, codeBlock: false,
horizontalRule: false, horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
dropcursor: { dropcursor: {
color: "rgba(var(--color-text-100))", color: "rgba(var(--color-text-100))",
width: 2, width: 2,
@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-lg border border-custom-border-300", class: "rounded-lg border border-custom-border-300",
}, },
}), }),
HorizontalRule,
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,
Color, Color,

View File

@ -4,13 +4,16 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({ Extension.create({
name: "enterKey", name: "enterKey",
addKeyboardShortcuts() { addKeyboardShortcuts(this) {
return { return {
Enter: () => { Enter: () => {
if (onEnterKeyPress) { if (!this.editor.storage.mentionsOpen) {
onEnterKeyPress(); if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
} }
return true; return false;
}, },
"Shift-Enter": ({ editor }) => "Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [ editor.commands.first(({ commands }) => [

View File

@ -1,5 +1,3 @@
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension"; import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [ export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
// EnterKeyExtension(onEnterKeyPress),
];

View File

@ -31,7 +31,6 @@ export const GoogleSignInButton: FC<Props> = (props) => {
size: "large", size: "large",
logo_alignment: "center", logo_alignment: "center",
text: type === "sign_in" ? "signin_with" : "signup_with", text: type === "sign_in" ? "signin_with" : "signup_with",
width: 384,
} as GsiButtonConfiguration // customization attributes } as GsiButtonConfiguration // customization attributes
); );
} catch (err) { } catch (err) {

View File

@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
onChange(unsplashImages[0].urls.regular); onChange(unsplashImages[0].urls.regular);
}, [value, onChange, unsplashImages]); }, [value, onChange, unsplashImages]);
const openDropdown = () => setIsOpen(true); const handleClose = () => {
const closeDropdown = () => setIsOpen(false); if (isOpen) setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); };
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 ( return (
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}> <Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
<Popover.Button <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" 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} disabled={disabled}
> >
{label} {label}

View File

@ -587,14 +587,16 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="relative h-40 w-80"> {cycleDetails && cycleDetails.distribution && (
<ProgressChart <div className="relative h-40 w-80">
distribution={cycleDetails.distribution?.completion_chart ?? {}} <ProgressChart
startDate={cycleDetails.start_date ?? ""} distribution={cycleDetails.distribution?.completion_chart ?? {}}
endDate={cycleDetails.end_date ?? ""} startDate={cycleDetails.start_date ?? ""}
totalIssues={cycleDetails.total_issues} endDate={cycleDetails.end_date ?? ""}
/> totalIssues={cycleDetails.total_issues}
</div> />
</div>
)}
</div> </div>
) : ( ) : (
"" ""

View File

@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
})} })}
> >
Issues Issues
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium py-1 px-1.5 rounded-xl h-4 min-w-6 flex items-center text-center justify-center"> <span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl h-4 min-w-6 flex items-center text-center justify-center">
{totalIssues} {totalIssues}
</span> </span>
</h6> </h6>

View File

@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
startedCount > 0 startedCount > 0
? "started" ? "started"
: unStartedCount > 0 : unStartedCount > 0
? "unstarted" ? "unstarted"
: backlogCount > 0 : backlogCount > 0
? "backlog" ? "backlog"
: completedCount > 0 : completedCount > 0
? "completed" ? "completed"
: canceledCount > 0 : canceledCount > 0
? "cancelled" ? "cancelled"
: null; : null;
setActiveStateGroup(stateGroup); setActiveStateGroup(stateGroup);
setDefaultStateGroup(stateGroup); setDefaultStateGroup(stateGroup);
@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
/> />
</div> </div>
{totalCount > 0 ? ( {totalCount > 0 ? (
<div className="flex items-center pl-20 md:pl-11 lg:pl-14 pr-11 mt-11"> <div className="flex items-center pl-10 md:pl-11 lg:pl-14 pr-11 mt-11">
<div className="flex md:flex-col lg:flex-row items-center gap-x-10 gap-y-8 w-full"> <div className="flex flex-col sm:flex-row md:flex-row lg:flex-row items-center justify-evenly gap-x-10 gap-y-8 w-full">
<div className="w-full flex justify-center"> <div>
<PieGraph <PieGraph
data={chartData} data={chartData}
height="220px" height="220px"
width="220px" width="200px"
innerRadius={0.6} innerRadius={0.6}
cornerRadius={5} cornerRadius={5}
colors={(datum) => datum.data.color} colors={(datum) => datum.data.color}
@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
layers={["arcs", CenteredMetric]} layers={["arcs", CenteredMetric]}
/> />
</div> </div>
<div className="justify-self-end space-y-6 w-min whitespace-nowrap"> <div className="space-y-6 w-min whitespace-nowrap">
{chartData.map((item) => ( {chartData.map((item) => (
<div key={item.id} className="flex items-center justify-between gap-6"> <div key={item.id} className="flex items-center justify-between gap-6">
<div className="flex items-center gap-2.5 w-24"> <div className="flex items-center gap-2.5 w-24">

View 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>
);
};

View 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",
];

View File

@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useCycle } from "hooks/store"; import { useApplication, useCycle } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons // icons
import { ContrastIcon, Tooltip } from "@plane/ui"; import { ContrastIcon } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { ICycle } from "@plane/types";
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
@ -24,18 +27,6 @@ type Props = TDropdownProps & {
value: string | null; 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 = type DropdownOptions =
| { | {
value: string | null; value: string | null;
@ -44,100 +35,6 @@ type DropdownOptions =
}[] }[]
| undefined; | 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) => { export const CycleDropdown: React.FC<Props> = observer((props) => {
const { const {
button, button,
@ -153,8 +50,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
placeholder = "Cycle", placeholder = "Cycle",
placement, placement,
projectId, projectId,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -221,13 +118,34 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
const selectedCycle = value ? getCycleById(value) : null; const selectedCycle = value ? getCycleById(value) : null;
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -236,7 +154,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full", className)} className={cn("h-full", className)}
value={value} value={value}
onChange={onChange} onChange={dropdownOnChange}
disabled={disabled} disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@ -246,7 +164,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -262,77 +180,24 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{/* TODO: move button components to a single file for each dropdown */} <DropdownButton
{buttonVariant === "border-with-text" ? ( className={buttonClassName}
<BorderButton isActive={isOpen}
cycle={selectedCycle} tooltipHeading="Cycle"
className={buttonClassName} tooltipContent={selectedCycle?.name ?? placeholder}
dropdownArrow={dropdownArrow && !disabled} showTooltip={showTooltip}
dropdownArrowClassName={dropdownArrowClassName} variant={buttonVariant}
hideIcon={hideIcon} >
placeholder={placeholder} {!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
isActive={isOpen} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
tooltip={tooltip} <span className="flex-grow truncate">{selectedCycle?.name ?? placeholder}</span>
/> )}
) : buttonVariant === "border-without-text" ? ( {dropdownArrow && (
<BorderButton <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
cycle={selectedCycle} )}
className={buttonClassName} </DropdownButton>
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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -366,7 +231,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -2,17 +2,19 @@ import React, { useRef, useState } from "react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Calendar, CalendarDays, X } from "lucide-react"; import { CalendarDays, X } from "lucide-react";
// hooks // hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // components
import { Tooltip } from "@plane/ui"; import { DropdownButton } from "./buttons";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
clearIconClassName?: string; clearIconClassName?: string;
@ -23,157 +25,6 @@ type Props = TDropdownProps & {
onChange: (val: Date | null) => void; onChange: (val: Date | null) => void;
value: Date | string | null; value: Date | string | null;
closeOnSelect?: boolean; 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) => { export const DateDropdown: React.FC<Props> = (props) => {
@ -193,9 +44,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
onChange, onChange,
placeholder = "Date", placeholder = "Date",
placement, placement,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
showPlaceholderIcon = false,
value, value,
} = props; } = props;
const [isOpen, setIsOpen] = useState(false); 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 = () => { const onOpen = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -247,90 +118,30 @@ export const DateDropdown: React.FC<Props> = (props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
date={value} isActive={isOpen}
className={buttonClassName} tooltipHeading={placeholder}
clearIconClassName={clearIconClassName} tooltipContent={value ? renderFormattedDate(value) : "None"}
hideIcon={hideIcon} showTooltip={showTooltip}
icon={icon} variant={buttonVariant}
placeholder={placeholder} >
isClearable={isClearable && isDateSelected} {!hideIcon && icon}
onClear={() => onChange(null)} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
isActive={isOpen} <span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
tooltip={tooltip} )}
/> {isClearable && isDateSelected && (
) : buttonVariant === "border-without-text" ? ( <X
<BorderButton className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
date={value} onClick={(e) => {
className={buttonClassName} e.stopPropagation();
clearIconClassName={clearIconClassName} onChange(null);
hideIcon={hideIcon} }}
icon={icon} />
placeholder={placeholder} )}
isClearable={isClearable && isDateSelected} </DropdownButton>
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}
</button> </button>
</Combobox.Button> </Combobox.Button>
{isOpen && ( {isOpen && (
@ -338,10 +149,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}> <div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<DatePicker <DatePicker
selected={value ? new Date(value) : null} selected={value ? new Date(value) : null}
onChange={(val) => { onChange={dropdownOnChange}
onChange(val);
if (closeOnSelect) closeDropdown();
}}
dateFormat="dd-MM-yyyy" dateFormat="dd-MM-yyyy"
minDate={minDate} minDate={minDate}
maxDate={maxDate} maxDate={maxDate}

View File

@ -8,12 +8,14 @@ import sortBy from "lodash/sortBy";
import { useApplication, useEstimate } from "hooks/store"; import { useApplication, useEstimate } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // components
import { Tooltip } from "@plane/ui"; import { DropdownButton } from "./buttons";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
@ -24,18 +26,6 @@ type Props = TDropdownProps & {
value: number | null; 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 = type DropdownOptions =
| { | {
value: number | null; value: number | null;
@ -44,118 +34,6 @@ type DropdownOptions =
}[] }[]
| undefined; | 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) => { export const EstimateDropdown: React.FC<Props> = observer((props) => {
const { const {
button, button,
@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
placeholder = "Estimate", placeholder = "Estimate",
placement, placement,
projectId, projectId,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null; const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId); if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -245,7 +143,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full w-full", className)} className={cn("h-full w-full", className)}
value={value} value={value}
onChange={onChange} onChange={dropdownOnChange}
disabled={disabled} disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@ -255,7 +153,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
estimatePoint={selectedEstimate} isActive={isOpen}
className={buttonClassName} tooltipHeading="Estimate"
dropdownArrow={dropdownArrow && !disabled} tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder}
dropdownArrowClassName={dropdownArrowClassName} showTooltip={showTooltip}
hideIcon={hideIcon} variant={buttonVariant}
placeholder={placeholder} >
isActive={isOpen} {!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
tooltip={tooltip} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
/> <span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span>
) : buttonVariant === "border-without-text" ? ( )}
<BorderButton {dropdownArrow && (
estimatePoint={selectedEstimate} <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
className={buttonClassName} )}
dropdownArrow={dropdownArrow && !disabled} </DropdownButton>
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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -3,7 +3,6 @@ export * from "./cycle";
export * from "./date"; export * from "./date";
export * from "./estimate"; export * from "./estimate";
export * from "./module"; export * from "./module";
export * from "./module-select";
export * from "./priority"; export * from "./priority";
export * from "./project"; export * from "./project";
export * from "./state"; export * from "./state";

View 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" />;
});

View File

@ -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>
);
});

View File

@ -1,3 +1,2 @@
export * from "./buttons";
export * from "./project-member"; export * from "./project-member";
export * from "./workspace-member"; export * from "./workspace-member";

View File

@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react"; import { Check, ChevronDown, Search } from "lucide-react";
// hooks // hooks
import { useApplication, useMember, useUser } from "hooks/store"; import { useApplication, useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons // icons
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { MemberDropdownProps } from "./types"; import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
type Props = { type Props = {
projectId: string; projectId: string;
@ -36,8 +39,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
placeholder = "Members", placeholder = "Members",
placement, placement,
projectId, projectId,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
}; };
if (multiple) comboboxProps.multiple = true; if (multiple) comboboxProps.multiple = true;
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId); if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -112,6 +135,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
ref={dropdownRef} ref={dropdownRef}
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full", className)} className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
{...comboboxProps} {...comboboxProps}
> >
@ -121,7 +145,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
userIds={value} isActive={isOpen}
className={buttonClassName} tooltipHeading={placeholder}
dropdownArrow={dropdownArrow && !disabled} tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
dropdownArrowClassName={dropdownArrowClassName} showTooltip={showTooltip}
hideIcon={hideIcon} variant={buttonVariant}
placeholder={placeholder} >
isActive={isOpen} {!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
tooltip={tooltip} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
/> <span className="flex-grow truncate text-sm leading-5">
) : buttonVariant === "border-without-text" ? ( {Array.isArray(value) && value.length > 0
<BorderButton ? value.length === 1
userIds={value} ? getUserDetails(value[0])?.display_name
className={buttonClassName} : ""
dropdownArrow={dropdownArrow && !disabled} : placeholder}
dropdownArrowClassName={dropdownArrowClassName} </span>
hideIcon={hideIcon} )}
placeholder={placeholder} {dropdownArrow && (
isActive={isOpen} <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
tooltip={tooltip} )}
hideText </DropdownButton>
/>
) : 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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={() => {
if (!multiple) closeDropdown();
}}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react"; import { Check, ChevronDown, Search } from "lucide-react";
// hooks // hooks
import { useMember, useUser } from "hooks/store"; import { useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons // icons
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { MemberDropdownProps } from "./types"; import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => { export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
const { const {
@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
onChange, onChange,
placeholder = "Members", placeholder = "Members",
placement, placement,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState<boolean>(false);
// refs // refs
const dropdownRef = useRef<HTMLDivElement | null>(null); const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs // popper-js refs
@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
}; };
if (multiple) comboboxProps.multiple = true; if (multiple) comboboxProps.multiple = true;
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -103,6 +127,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
className={cn("h-full", className)} className={cn("h-full", className)}
{...comboboxProps} {...comboboxProps}
handleKeyDown={handleKeyDown} handleKeyDown={handleKeyDown}
onChange={dropdownOnChange}
> >
<Combobox.Button as={Fragment}> <Combobox.Button as={Fragment}>
{button ? ( {button ? (
@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -125,124 +151,82 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
userIds={value} isActive={isOpen}
className={buttonClassName} tooltipHeading={placeholder}
dropdownArrow={dropdownArrow && !disabled} tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
dropdownArrowClassName={dropdownArrowClassName} showTooltip={showTooltip}
hideIcon={hideIcon} variant={buttonVariant}
placeholder={placeholder} >
tooltip={tooltip} {!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
/> {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
) : buttonVariant === "border-without-text" ? ( <span className="flex-grow truncate text-sm leading-5">
<BorderButton {Array.isArray(value) && value.length > 0
userIds={value} ? value.length === 1
className={buttonClassName} ? getUserDetails(value[0])?.display_name
dropdownArrow={dropdownArrow && !disabled} : ""
dropdownArrowClassName={dropdownArrowClassName} : placeholder}
hideIcon={hideIcon} </span>
placeholder={placeholder} )}
tooltip={tooltip} {dropdownArrow && (
hideText <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
/> )}
) : buttonVariant === "background-with-text" ? ( </DropdownButton>
<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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
<Combobox.Options className="fixed z-10"> {isOpen && (
<div <Combobox.Options className="fixed z-10" static>
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" <div
ref={setPopperElement} 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"
style={styles.popper} ref={setPopperElement}
{...attributes.popper} 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} /> <div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Combobox.Input <Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none" <Combobox.Input
value={query} className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
onChange={(e) => setQuery(e.target.value)} value={query}
placeholder="Search" onChange={(e) => setQuery(e.target.value)}
displayValue={(assigned: any) => assigned?.name} placeholder="Search"
/> displayValue={(assigned: any) => assigned?.name}
</div> />
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> </div>
{filteredOptions ? ( <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
filteredOptions.length > 0 ? ( {filteredOptions ? (
filteredOptions.map((option) => ( filteredOptions.length > 0 ? (
<Combobox.Option filteredOptions.map((option) => (
key={option.value} <Combobox.Option
value={option.value} key={option.value}
className={({ active, selected }) => value={option.value}
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ className={({ active, selected }) =>
active ? "bg-custom-background-80" : "" `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` active ? "bg-custom-background-80" : ""
} } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
onClick={() => { }
if (!multiple) closeDropdown(); >
}} {({ selected }) => (
> <>
{({ selected }) => ( <span className="flex-grow truncate">{option.content}</span>
<> {selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
<span className="flex-grow truncate">{option.content}</span> </>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />} )}
</> </Combobox.Option>
)} ))
</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">No matching results</p> <p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
) )}
) : ( </div>
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div> </div>
</div> </Combobox.Options>
</Combobox.Options> )}
</Combobox> </Combobox>
); );
}); });

View File

@ -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>
);
});

View File

@ -1,2 +0,0 @@
export * from "./button";
export * from "./select";

View File

@ -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>
);
});

View File

@ -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;
};

View File

@ -2,27 +2,40 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react"; import { Check, ChevronDown, Search, X } from "lucide-react";
// hooks // hooks
import { useApplication, useModule } from "hooks/store"; import { useApplication, useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons // icons
import { DiceIcon, Tooltip } from "@plane/ui"; import { DiceIcon, Tooltip } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { IModule } from "@plane/types";
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
dropdownArrow?: boolean; dropdownArrow?: boolean;
dropdownArrowClassName?: string; dropdownArrowClassName?: string;
onChange: (val: string | null) => void;
projectId: string; projectId: string;
value: string | null; showCount?: boolean;
}; } & (
| {
multiple: false;
onChange: (val: string | null) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[];
}
);
type DropdownOptions = type DropdownOptions =
| { | {
@ -32,110 +45,97 @@ type DropdownOptions =
}[] }[]
| undefined; | undefined;
type ButtonProps = { type ButtonContentProps = {
className?: string; disabled: boolean;
dropdownArrow: boolean; dropdownArrow: boolean;
dropdownArrowClassName: string; dropdownArrowClassName: string;
hideIcon?: boolean; hideIcon: boolean;
hideText?: boolean; hideText: boolean;
isActive?: boolean; onChange: (moduleIds: string[]) => void;
module: IModule | null;
placeholder: string; placeholder: string;
tooltip: boolean; showCount: boolean;
value: string | string[] | null;
}; };
const BorderButton = (props: ButtonProps) => { const ButtonContent: React.FC<ButtonContentProps> = (props) => {
const { const {
className, disabled,
dropdownArrow, dropdownArrow,
dropdownArrowClassName, dropdownArrowClassName,
hideIcon = false, hideIcon,
hideText = false, hideText,
isActive = false, onChange,
module,
placeholder, placeholder,
tooltip, showCount,
value,
} = props; } = props;
// store hooks
const { getModuleById } = useModule();
return ( if (Array.isArray(value))
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}> return (
<div <>
className={cn( {showCount ? (
"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 }, {!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
className <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>
);
})}
</div>
) : (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
<span className="flex-grow truncate text-left">{placeholder}</span>
</>
)} )}
>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
{dropdownArrow && ( {dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" /> <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)} )}
</div> </>
</Tooltip> );
); else
}; return (
<>
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
)}
>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />} {!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 && ( {dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" /> <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;
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>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
}; };
export const ModuleDropdown: React.FC<Props> = observer((props) => { export const ModuleDropdown: React.FC<Props> = observer((props) => {
@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
dropdownArrow = false, dropdownArrow = false,
dropdownArrowClassName = "", dropdownArrowClassName = "",
hideIcon = false, hideIcon = false,
multiple,
onChange, onChange,
placeholder = "Module", placeholder = "Module",
placement, placement,
projectId, projectId,
showCount = false,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -186,7 +188,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
const options: DropdownOptions = moduleIds?.map((moduleId) => { const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId); const moduleDetails = getModuleById(moduleId);
return { return {
value: moduleId, value: moduleId,
query: `${moduleDetails?.name}`, query: `${moduleDetails?.name}`,
@ -198,16 +199,17 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
), ),
}; };
}); });
options?.unshift({ if (!multiple)
value: null, options?.unshift({
query: "No module", value: null,
content: ( query: "No module",
<div className="flex items-center gap-2"> content: (
<DiceIcon className="h-3 w-3 flex-shrink-0" /> <div className="flex items-center gap-2">
<span className="flex-grow truncate">No module</span> <DiceIcon className="h-3 w-3 flex-shrink-0" />
</div> <span className="flex-grow truncate">No module</span>
), </div>
}); ),
});
const filteredOptions = const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
@ -219,15 +221,41 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
if (!moduleIds) fetchModules(workspaceSlug, projectId); if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]); }, [moduleIds, fetchModules, projectId, workspaceSlug]);
const selectedModule = value ? getModuleById(value) : null; const onOpen = () => {
const openDropdown = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -235,10 +263,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
ref={dropdownRef} ref={dropdownRef}
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full", className)} className={cn("h-full", className)}
value={value}
onChange={onChange}
disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
{...comboboxProps}
> >
<Combobox.Button as={Fragment}> <Combobox.Button as={Fragment}>
{button ? ( {button ? (
@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
module={selectedModule} isActive={isOpen}
className={buttonClassName} tooltipHeading="Module"
dropdownArrow={dropdownArrow && !disabled} tooltipContent={
Array.isArray(value) ? `${value?.length ?? 0} module${value?.length !== 1 ? "s" : ""}` : ""
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<ButtonContent
disabled={disabled}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName} dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon} hideIcon={hideIcon}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder} placeholder={placeholder}
isActive={isOpen} showCount={showCount}
tooltip={tooltip} value={value}
// @ts-ignore
onChange={onChange}
/> />
) : buttonVariant === "border-without-text" ? ( </DropdownButton>
<BorderButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : 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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
key={option.value} key={option.value}
value={option.value} value={option.value}
className={({ active, selected }) => className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ cn(
active ? "bg-custom-background-80" : "" "w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` {
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
}
)
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -15,6 +15,7 @@ import { TIssuePriorities } from "@plane/types";
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants // constants
import { ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_PRIORITIES } from "constants/issue";
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
@ -34,7 +35,7 @@ type ButtonProps = {
isActive?: boolean; isActive?: boolean;
highlightUrgent: boolean; highlightUrgent: boolean;
priority: TIssuePriorities; priority: TIssuePriorities;
tooltip: boolean; showTooltip: boolean;
}; };
const BorderButton = (props: ButtonProps) => { const BorderButton = (props: ButtonProps) => {
@ -46,7 +47,7 @@ const BorderButton = (props: ButtonProps) => {
hideText = false, hideText = false,
highlightUrgent, highlightUrgent,
priority, priority,
tooltip, showTooltip,
} = props; } = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@ -60,7 +61,7 @@ const BorderButton = (props: ButtonProps) => {
}; };
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5", "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, hideText = false,
highlightUrgent, highlightUrgent,
priority, priority,
tooltip, showTooltip,
} = props; } = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@ -129,7 +130,7 @@ const BackgroundButton = (props: ButtonProps) => {
}; };
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5", "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, isActive = false,
highlightUrgent, highlightUrgent,
priority, priority,
tooltip, showTooltip,
} = props; } = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@ -199,7 +200,7 @@ const TransparentButton = (props: ButtonProps) => {
}; };
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80", "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, highlightUrgent = true,
onChange, onChange,
placement, placement,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
const filteredOptions = const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -323,7 +351,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
className className
)} )}
value={value} value={value}
onChange={onChange} onChange={dropdownOnChange}
disabled={disabled} disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -349,86 +377,20 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <ButtonToRender
<BorderButton priority={value}
priority={value} className={cn(buttonClassName, {
className={cn(buttonClassName, { "text-white": resolvedTheme === "dark",
"text-white": resolvedTheme === "dark", })}
})} highlightUrgent={highlightUrgent}
highlightUrgent={highlightUrgent} dropdownArrow={dropdownArrow && !disabled}
dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName}
dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon}
hideIcon={hideIcon} showTooltip={showTooltip}
tooltip={tooltip} 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> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -7,14 +7,15 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // components
import { Tooltip } from "@plane/ui"; import { DropdownButton } from "./buttons";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { IProject } from "@plane/types";
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
@ -24,119 +25,6 @@ type Props = TDropdownProps & {
value: string | null; 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) => { export const ProjectDropdown: React.FC<Props> = observer((props) => {
const { const {
button, button,
@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
onChange, onChange,
placeholder = "Project", placeholder = "Project",
placement, placement,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
const selectedProject = value ? getProjectById(value) : null; const selectedProject = value ? getProjectById(value) : null;
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -219,7 +128,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full", className)} className={cn("h-full", className)}
value={value} value={value}
onChange={onChange} onChange={dropdownOnChange}
disabled={disabled} disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@ -229,7 +138,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
project={selectedProject} isActive={isOpen}
className={buttonClassName} tooltipHeading="Project"
dropdownArrow={dropdownArrow && !disabled} tooltipContent={selectedProject?.name ?? placeholder}
dropdownArrowClassName={dropdownArrowClassName} showTooltip={showTooltip}
hideIcon={hideIcon} variant={buttonVariant}
placeholder={placeholder} >
tooltip={tooltip} {!hideIcon && (
/> <span className="grid place-items-center flex-shrink-0">
) : buttonVariant === "border-without-text" ? ( {selectedProject?.emoji
<BorderButton ? renderEmoji(selectedProject?.emoji)
project={selectedProject} : selectedProject?.icon_prop
className={buttonClassName} ? renderEmoji(selectedProject?.icon_prop)
dropdownArrow={dropdownArrow && !disabled} : null}
dropdownArrowClassName={dropdownArrowClassName} </span>
hideIcon={hideIcon} )}
hideText {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
placeholder={placeholder} <span className="flex-grow truncate">{selectedProject?.name ?? placeholder}</span>
tooltip={tooltip} )}
/> {dropdownArrow && (
) : buttonVariant === "background-with-text" ? ( <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
<BackgroundButton )}
project={selectedProject} </DropdownButton>
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}
</button> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useProjectState } from "hooks/store"; import { useApplication, useProjectState } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons // icons
import { StateGroupIcon, Tooltip } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// types // types
import { IState } from "@plane/types";
import { TDropdownProps } from "./types"; import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & { type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
@ -24,130 +27,6 @@ type Props = TDropdownProps & {
value: string; 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) => { export const StateDropdown: React.FC<Props> = observer((props) => {
const { const {
button, button,
@ -162,8 +41,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
onChange, onChange,
placement, placement,
projectId, projectId,
showTooltip = false,
tabIndex, tabIndex,
tooltip = false,
value, value,
} = props; } = props;
// states // states
@ -209,14 +88,35 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const selectedState = getStateById(value); const selectedState = getStateById(value);
const openDropdown = () => { const onOpen = () => {
setIsOpen(true);
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId); if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -225,7 +125,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("h-full", className)} className={cn("h-full", className)}
value={value} value={value}
onChange={onChange} onChange={dropdownOnChange}
disabled={disabled} disabled={disabled}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@ -235,7 +135,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)} className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown} onClick={handleOnClick}
> >
{button} {button}
</button> </button>
@ -251,70 +151,30 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
}, },
buttonContainerClassName buttonContainerClassName
)} )}
onClick={openDropdown} onClick={handleOnClick}
> >
{buttonVariant === "border-with-text" ? ( <DropdownButton
<BorderButton className={buttonClassName}
state={selectedState} isActive={isOpen}
className={buttonClassName} tooltipHeading="State"
dropdownArrow={dropdownArrow && !disabled} tooltipContent={selectedState?.name ?? "State"}
dropdownArrowClassName={dropdownArrowClassName} showTooltip={showTooltip}
hideIcon={hideIcon} variant={buttonVariant}
isActive={isOpen} >
tooltip={tooltip} {!hideIcon && (
/> <StateGroupIcon
) : buttonVariant === "border-without-text" ? ( stateGroup={selectedState?.group ?? "backlog"}
<BorderButton color={selectedState?.color}
state={selectedState} className="h-3 w-3 flex-shrink-0"
className={buttonClassName} />
dropdownArrow={dropdownArrow && !disabled} )}
dropdownArrowClassName={dropdownArrowClassName} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
hideIcon={hideIcon} <span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
isActive={isOpen} )}
tooltip={tooltip} {dropdownArrow && (
hideText <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
/> )}
) : buttonVariant === "background-with-text" ? ( </DropdownButton>
<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> </button>
)} )}
</Combobox.Button> </Combobox.Button>
@ -348,7 +208,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={closeDropdown}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -17,7 +17,6 @@ export type TDropdownProps = {
hideIcon?: boolean; hideIcon?: boolean;
placeholder?: string; placeholder?: string;
placement?: Placement; placement?: Placement;
showTooltip?: boolean;
tabIndex?: number; tabIndex?: number;
// TODO: rename this prop to showTooltip
tooltip?: boolean;
}; };

View File

@ -2,13 +2,13 @@ import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// ui // ui
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs, CustomMenu } from "@plane/ui";
// helper // helper
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useProject, useUser } from "hooks/store"; import { useProject, useUser } from "hooks/store";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
// components // components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@ -20,7 +20,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
const { title } = props; const { title } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
@ -31,29 +31,48 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<SidebarHamburgerToggle />
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div> <div>
<Breadcrumbs> <div className="z-50">
<Breadcrumbs.BreadcrumbItem <Breadcrumbs>
type="text" <Breadcrumbs.BreadcrumbItem
label={currentProjectDetails?.name ?? "Project"} type="text"
icon={ label={currentProjectDetails?.name ?? "Project"}
currentProjectDetails?.emoji ? ( icon={
renderEmoji(currentProjectDetails.emoji) currentProjectDetails?.emoji ? (
) : currentProjectDetails?.icon_prop ? ( renderEmoji(currentProjectDetails.emoji)
renderEmoji(currentProjectDetails.icon_prop) ) : currentProjectDetails?.icon_prop ? (
) : ( renderEmoji(currentProjectDetails.icon_prop)
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> ) : (
{currentProjectDetails?.name.charAt(0)} <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
</span> {currentProjectDetails?.name.charAt(0)}
) </span>
} )
link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} }
/> link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
<Breadcrumbs.BreadcrumbItem type="text" label={title} /> />
</Breadcrumbs> <div className="hidden sm:hidden md:block lg:block">
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</div>
</Breadcrumbs>
</div>
</div> </div>
<CustomMenu
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
maxHeight="lg"
customButton={
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">{title}</span>
}
placement="bottom-start"
closeOnSelect
>
{PROJECT_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div> </div>
</div> </div>
); );

View File

@ -1,12 +1,13 @@
import { FC } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// ui // ui
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
// hooks // hooks
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
export interface IWorkspaceSettingHeader { export interface IWorkspaceSettingHeader {
title: string; title: string;
@ -21,7 +22,7 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/> <SidebarHamburgerToggle />
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
@ -30,9 +31,26 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
icon={<Settings className="h-4 w-4 text-custom-text-300" />} icon={<Settings className="h-4 w-4 text-custom-text-300" />}
link={`/${workspaceSlug}/settings`} link={`/${workspaceSlug}/settings`}
/> />
<Breadcrumbs.BreadcrumbItem type="text" label={title} /> <div className="hidden sm:hidden md:block lg:block">
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</div>
</Breadcrumbs> </Breadcrumbs>
</div> </div>
<CustomMenu
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
maxHeight="lg"
customButton={
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">{title}</span>
}
placement="bottom-start"
closeOnSelect
>
{WORKSPACE_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}${item.href}`)}>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div> </div>
</div> </div>
); );

View File

@ -7,6 +7,8 @@ import { Plus } from "lucide-react";
import { useApplication, useProject } from "hooks/store"; import { useApplication, useProject } from "hooks/store";
// components // components
import { CustomSelect, Input } from "@plane/ui"; import { CustomSelect, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IJiraImporterForm } from "@plane/types"; import { IJiraImporterForm } from "@plane/types";
@ -46,17 +48,18 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange, ref } }) => (
<Input <Input
id="metadata.api_token" id="metadata.api_token"
name="metadata.api_token"
type="text" type="text"
value={value} value={value}
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.metadata?.api_token)}
placeholder="XXXXXXXX" placeholder="XXXXXXXX"
className="w-full" className="w-full"
autoComplete="off" autoComplete="off"
/> />
)} )}
/> />
{errors.metadata?.api_token && <p className="text-red-500 text-xs">{errors.metadata.api_token.message}</p>}
</div> </div>
</div> </div>
@ -75,7 +78,6 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange, ref } }) => (
<Input <Input
id="metadata.project_key" id="metadata.project_key"
name="metadata.project_key"
type="text" type="text"
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -86,6 +88,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/> />
)} )}
/> />
{errors.metadata?.project_key && (
<p className="text-red-500 text-xs">{errors.metadata.project_key.message}</p>
)}
</div> </div>
</div> </div>
@ -100,11 +105,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.email" name="metadata.email"
rules={{ rules={{
required: "Please enter email address.", required: "Please enter email address.",
validate: (value) => checkEmailValidity(value) || "Please enter a valid email address",
}} }}
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange, ref } }) => (
<Input <Input
id="metadata.email" id="metadata.email"
name="metadata.email"
type="email" type="email"
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -115,6 +120,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/> />
)} )}
/> />
{errors.metadata?.email && <p className="text-red-500 text-xs">{errors.metadata.email.message}</p>}
</div> </div>
</div> </div>
@ -129,12 +135,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.cloud_hostname" name="metadata.cloud_hostname"
rules={{ rules={{
required: "Please enter your cloud host name.", required: "Please enter your cloud host name.",
validate: (value) => !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://",
}} }}
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange, ref } }) => (
<Input <Input
id="metadata.cloud_hostname" id="metadata.cloud_hostname"
name="metadata.cloud_hostname"
type="email"
value={value} value={value}
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
@ -144,6 +149,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/> />
)} )}
/> />
{errors.metadata?.cloud_hostname && (
<p className="text-red-500 text-xs">{errors.metadata.cloud_hostname.message}</p>
)}
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@ import {
CycleDropdown, CycleDropdown,
DateDropdown, DateDropdown,
EstimateDropdown, EstimateDropdown,
ModuleSelectDropdown, ModuleDropdown,
PriorityDropdown, PriorityDropdown,
ProjectDropdown, ProjectDropdown,
ProjectMemberDropdown, ProjectMemberDropdown,
@ -257,14 +257,16 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
}; };
useEffect(() => { useEffect(() => {
setFocus("name");
reset({ reset({
...defaultValues, ...defaultValues,
...(prePopulatedData ?? {}), ...(prePopulatedData ?? {}),
...(data ?? {}), ...(data ?? {}),
}); });
}, [setFocus, prePopulatedData, reset, data]); }, [prePopulatedData, reset, data]);
useEffect(() => {
setFocus("name");
}, [setFocus]);
// update projectId in form when projectId changes // update projectId in form when projectId changes
useEffect(() => { useEffect(() => {
@ -577,12 +579,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
name="module_ids" name="module_ids"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<div className="h-7"> <div className="h-7">
<ModuleSelectDropdown <ModuleDropdown
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId} projectId={projectId}
value={value || undefined} value={value ?? []}
onChange={(moduleId) => onChange(moduleId)} onChange={onChange}
buttonVariant="border-with-text" buttonVariant="border-with-text"
multiple
/> />
</div> </div>
)} )}

View File

@ -10,7 +10,7 @@ import { TActivityOperations } from "../root";
import { TIssueComment } from "@plane/types"; import { TIssueComment } from "@plane/types";
// icons // icons
import { Globe2, Lock } from "lucide-react"; import { Globe2, Lock } from "lucide-react";
import { useWorkspace } from "hooks/store"; import { useMention, useWorkspace } from "hooks/store";
const fileService = new FileService(); const fileService = new FileService();
@ -43,6 +43,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
const workspaceStore = useWorkspace(); const workspaceStore = useWorkspace();
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
const { mentionHighlights, mentionSuggestions } = useMention();
// refs // refs
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
// react hook form // react hook form
@ -61,7 +63,14 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
}; };
return ( return (
<div> <div
// onKeyDown={(e) => {
// if (e.key === "Enter" && !e.shiftKey) {
// e.preventDefault();
// // handleSubmit(onSubmit)(e);
// }
// }}
>
<Controller <Controller
name="access" name="access"
control={control} control={control}
@ -72,6 +81,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<LiteTextEditorWithRef <LiteTextEditorWithRef
onEnterKeyPress={(e) => { onEnterKeyPress={(e) => {
console.log("yo");
handleSubmit(onSubmit)(e); handleSubmit(onSubmit)(e);
}} }}
cancelUploadImage={fileService.cancelUpload} cancelUploadImage={fileService.cancelUpload}
@ -86,6 +96,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
onChange={(comment_json: Object, comment_html: string) => { onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html); onChange(comment_html);
}} }}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
commentAccessSpecifier={ commentAccessSpecifier={
showAccessSpecifier showAccessSpecifier
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess } ? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }

View File

@ -1,10 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import xor from "lodash/xor";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
// components // components
import { ModuleSelectDropdown } from "components/dropdowns"; import { ModuleDropdown } from "components/dropdowns";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// helpers // helpers
@ -33,56 +32,42 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
const disableSelect = disabled || isUpdating; const disableSelect = disabled || isUpdating;
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => { const handleIssueModuleChange = async (moduleIds: string[]) => {
if (!issue) return; if (!issue || !issue.module_ids) return;
setIsUpdating(true); 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) if (moduleIds.length === 0)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]); await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids);
else if (moduleIds.length > issue.module_ids.length) {
if (Array.isArray(moduleIds)) { const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m));
if (moduleIds.includes(undefined)) { await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds);
await issueOperations.removeModulesFromIssue?.( } else if (moduleIds.length < issue.module_ids.length) {
workspaceSlug, const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m));
projectId, await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds);
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); setIsUpdating(false);
}; };
return ( return (
<div className={cn(`flex items-center gap-1 h-full`, className)}> <div className={cn(`flex items-center gap-1 h-full`, className)}>
<ModuleSelectDropdown <ModuleDropdown
workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
value={issue?.module_ids?.length ? issue?.module_ids : undefined} value={issue?.module_ids ?? []}
onChange={handleIssueModuleChange} onChange={handleIssueModuleChange}
multiple={true}
placeholder="No module" placeholder="No module"
disabled={disableSelect} disabled={disableSelect}
className={`w-full h-full group`} className="w-full h-full group"
buttonContainerClassName="w-full" 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" buttonVariant="transparent-with-text"
hideIcon={false} hideIcon
dropdownArrow={true} dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
showTooltip={true} showTooltip
multiple
/> />
{isUpdating && <Spinner className="h-4 w-4" />} {isUpdating && <Spinner className="h-4 w-4" />}
</div> </div>
); );

View File

@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`} href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs font-medium mt-0.5" className="text-xs font-medium"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id} {parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}

View File

@ -132,7 +132,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`} href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs font-medium mt-0.5" className="text-xs font-medium"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`} {`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}

View File

@ -198,15 +198,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
try { try {
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({ setToastAlert({
title: "Module removed from issue successfully",
type: "success", type: "success",
message: "Module removed from issue successfully", title: "Successful!",
message: "Issue removed from module successfully.",
}); });
} catch (error) { } catch (error) {
setToastAlert({ setToastAlert({
title: "Module remove from issue failed",
type: "error", type: "error",
message: "Module remove from issue failed", title: "Error!",
message: "Issue could not be removed from module. Please try again.",
}); });
} }
}, },

View File

@ -233,7 +233,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon // TODO: add this logic
// showPlaceholderIcon
/> />
</div> </div>
@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon // TODO: add this logic
// showPlaceholderIcon
/> />
</div> </div>

View File

@ -61,7 +61,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
projectId?.toString(), projectId?.toString(),
issueStore, issueStore,
issueMap, issueMap,
groupedIssueIds groupedIssueIds,
viewId
).catch((err) => { ).catch((err) => {
setToastAlert({ setToastAlert({
title: "Error", title: "Error",

View File

@ -81,7 +81,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id} projectId={issue.project_id}
disabled={isReadOnly} disabled={isReadOnly}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
@ -95,7 +95,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
disabled={isReadOnly} disabled={isReadOnly}
buttonVariant="border-without-text" buttonVariant="border-without-text"
buttonClassName="border" buttonClassName="border"
tooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
@ -123,7 +123,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
placeholder="Start date" placeholder="Start date"
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly} disabled={isReadOnly}
tooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
@ -139,7 +139,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
placeholder="Due date" placeholder="Due date"
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly} disabled={isReadOnly}
tooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
@ -169,7 +169,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id} projectId={issue.project_id}
disabled={isReadOnly} disabled={isReadOnly}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>

View File

@ -20,7 +20,7 @@ import {
CycleDropdown, CycleDropdown,
DateDropdown, DateDropdown,
EstimateDropdown, EstimateDropdown,
ModuleSelectDropdown, ModuleDropdown,
PriorityDropdown, PriorityDropdown,
ProjectDropdown, ProjectDropdown,
ProjectMemberDropdown, ProjectMemberDropdown,
@ -267,6 +267,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange(); handleFormChange();
}} }}
buttonVariant="border-with-text" buttonVariant="border-with-text"
// TODO: update tabIndex logic
tabIndex={19} tabIndex={19}
/> />
</div> </div>
@ -547,18 +548,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
name="module_ids" name="module_ids"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<div className="h-7"> <div className="h-7">
<ModuleSelectDropdown <ModuleDropdown
workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId}
value={value || undefined} value={value ?? []}
onChange={(moduleId) => { onChange={(moduleIds) => {
onChange(moduleId); onChange(moduleIds);
handleFormChange(); handleFormChange();
}} }}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tabIndex={13} tabIndex={13}
multiple={true} multiple
showCount={true} showCount
/> />
</div> </div>
)} )}

View File

@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon // TODO: add this logic
// showPlaceholderIcon
/> />
</div> </div>
@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon // TODO: add this logic
// showPlaceholderIcon
/> />
</div> </div>

View File

@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const filteredOptions = const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
const openDropdown = () => { const onOpen = () => {
setIsDropdownOpen(true);
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId); if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsDropdownOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen); const handleClose = () => {
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Combobox <Combobox
@ -62,7 +82,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
ref={dropdownRef} ref={dropdownRef}
tabIndex={tabIndex} tabIndex={tabIndex}
value={value} value={value}
onChange={(val) => onChange(val)} onChange={dropdownOnChange}
className="relative flex-shrink-0 h-full" className="relative flex-shrink-0 h-full"
multiple multiple
disabled={disabled} disabled={disabled}
@ -73,7 +93,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
type="button" type="button"
ref={setReferenceElement} ref={setReferenceElement}
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200" className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
onClick={openDropdown} onClick={handleOnClick}
> >
{label ? ( {label ? (
label label

View File

@ -147,10 +147,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="relative mt-6 h-44 w-full"> <div className="relative mt-6 h-44 w-full">
<div className="absolute inset-0 z-[1] bg-gradient-to-t from-black/50 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" /> <img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
<div className="absolute bottom-4 z-10 flex w-full items-end justify-between gap-3 px-4"> <div className="absolute bottom-4 z-5 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate"> <div className="flex flex-grow gap-3 truncate">
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90"> <div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
<div className="grid h-7 w-7 place-items-center"> <div className="grid h-7 w-7 place-items-center">

View File

@ -1,4 +1,4 @@
import { Fragment } from "react"; import { Fragment, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
@ -6,6 +6,7 @@ import { useTheme } from "next-themes";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { mutate } from "swr"; import { mutate } from "swr";
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
import { usePopper } from "react-popper";
// hooks // hooks
import { useApplication, useUser, useWorkspace } from "hooks/store"; import { useApplication, useUser, useWorkspace } from "hooks/store";
// hooks // hooks
@ -14,7 +15,6 @@ import useToast from "hooks/use-toast";
import { Avatar, Loader } from "@plane/ui"; import { Avatar, Loader } from "@plane/ui";
// types // types
import { IWorkspace } from "@plane/types"; import { IWorkspace } from "@plane/types";
// Static Data // Static Data
const userLinks = (workspaceSlug: string, userId: string) => [ const userLinks = (workspaceSlug: string, userId: string) => [
{ {
@ -36,7 +36,6 @@ const userLinks = (workspaceSlug: string, userId: string) => [
icon: Settings, icon: Settings,
}, },
]; ];
const profileLinks = (workspaceSlug: string, userId: string) => [ const profileLinks = (workspaceSlug: string, userId: string) => [
{ {
name: "View profile", name: "View profile",
@ -49,7 +48,6 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
link: "/profile", link: "/profile",
}, },
]; ];
export const WorkspaceSidebarDropdown = observer(() => { export const WorkspaceSidebarDropdown = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
@ -64,12 +62,25 @@ export const WorkspaceSidebarDropdown = observer(() => {
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { setTheme } = useTheme(); const { setTheme } = useTheme();
// 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: "right",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const handleWorkspaceNavigation = (workspace: IWorkspace) => const handleWorkspaceNavigation = (workspace: IWorkspace) =>
updateCurrentUser({ updateCurrentUser({
last_workspace_id: workspace?.id, last_workspace_id: workspace?.id,
}); });
const handleSignOut = async () => { const handleSignOut = async () => {
await signOut() await signOut()
.then(() => { .then(() => {
@ -85,16 +96,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
}) })
); );
}; };
const handleItemClick = () => { const handleItemClick = () => {
console.log('CLICKED')
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
toggleSidebar(); toggleSidebar();
} }
}; };
const workspacesList = Object.values(workspaces ?? {}); const workspacesList = Object.values(workspaces ?? {});
// TODO: fix workspaces list scroll // TODO: fix workspaces list scroll
return ( return (
<div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4"> <div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4">
@ -121,14 +128,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
activeWorkspace?.name?.charAt(0) ?? "..." activeWorkspace?.name?.charAt(0) ?? "..."
)} )}
</div> </div>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<h4 className="truncate text-base font-medium text-custom-text-100"> <h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."} {activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
</h4> </h4>
)} )}
</div> </div>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<ChevronDown <ChevronDown
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : "" className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : ""
@ -137,7 +142,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
)} )}
</div> </div>
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
@ -185,7 +189,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
workspace?.name?.charAt(0) ?? "..." workspace?.name?.charAt(0) ?? "..."
)} )}
</span> </span>
<h5 <h5
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200" className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`} }`}
@ -226,9 +229,14 @@ export const WorkspaceSidebarDropdown = observer(() => {
</Menu.Item> </Menu.Item>
</Link> </Link>
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={link.key} href={link.href} className="w-full" onClick={() => { <Link
if (index > 0) handleItemClick(); key={link.key}
}}> href={link.href}
className="w-full"
onClick={() => {
if (index > 0) handleItemClick();
}}
>
<Menu.Item <Menu.Item
as="div" as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 font-medium" className="flex items-center gap-2 rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 font-medium"
@ -256,10 +264,9 @@ export const WorkspaceSidebarDropdown = observer(() => {
</> </>
)} )}
</Menu> </Menu>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0"> <Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none"> <Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
<Avatar <Avatar
name={currentUser?.display_name} name={currentUser?.display_name}
src={currentUser?.avatar} src={currentUser?.avatar}
@ -268,7 +275,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
className="!text-base" className="!text-base"
/> />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
@ -281,11 +287,20 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Menu.Items <Menu.Items
className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none" divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
> >
<div className="flex flex-col gap-2.5 pb-2"> <div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span> <span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( {profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={index} href={link.link} onClick={() => { if (index == 0) handleItemClick(); }}> <Link
key={index}
href={link.link}
onClick={() => {
if (index == 0) handleItemClick();
}}
>
<Menu.Item key={index} as="div"> <Menu.Item key={index} as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"> <span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<link.icon className="h-4 w-4 stroke-[1.5]" /> <link.icon className="h-4 w-4 stroke-[1.5]" />
@ -323,4 +338,4 @@ export const WorkspaceSidebarDropdown = observer(() => {
)} )}
</div> </div>
); );
}); });

View File

@ -1,23 +1,23 @@
import { useCallback } from "react"; import { useCallback } from "react";
type TUseDropdownKeyDown = { 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( const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => { (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter") { if (event.key === "Enter") {
event.stopPropagation(); event.stopPropagation();
if (!isOpen) { event.preventDefault();
onOpen(); onEnterKeyDown();
} } else if (event.key === "Escape") {
} else if (event.key === "Escape" && isOpen) {
event.stopPropagation(); event.stopPropagation();
onClose(); event.preventDefault();
onEscKeyDown();
} }
}, },
[isOpen, onOpen, onClose] [onEnterKeyDown, onEscKeyDown]
); );
return handleKeyDown; return handleKeyDown;

View File

@ -41,11 +41,13 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props)
} }
/> />
) : ( ) : (
<div className="flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll"> <div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8"> <div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar /> <ProjectSettingsSidebar />
</div> </div>
{children} <div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
{children}
</div>
</div> </div>
); );
}); });

View File

@ -10,11 +10,13 @@ export const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = (props) => {
const { children } = props; const { children } = props;
return ( return (
<div className="flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll"> <div className="inset-y-0 z-20 flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8"> <div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<WorkspaceSettingsSidebar /> <WorkspaceSettingsSidebar />
</div> </div>
{children} <div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
{children}
</div>
</div> </div>
); );
}; };

View File

@ -200,6 +200,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
if (!cycleId) throw new Error("Cycle Id is required"); if (!cycleId) throw new Error("Cycle Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
@ -267,9 +268,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
}); });
runInAction(() => { runInAction(() => {
update(this.issues, cycleId, (cycleIssueIds = []) => { update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
return uniq(concat(cycleIssueIds, issueIds));
});
}); });
issueIds.forEach((issueId) => { issueIds.forEach((issueId) => {
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });

View File

@ -205,6 +205,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
if (!moduleId) throw new Error("Module Id is required"); if (!moduleId) throw new Error("Module Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);

View File

@ -38,6 +38,8 @@ export interface IIssueRootStore {
members: string[] | undefined; members: string[] | undefined;
projects: string[] | undefined; projects: string[] | undefined;
rootStore: RootStore;
issues: IIssueStore; issues: IIssueStore;
state: IStateStore; state: IStateStore;
@ -87,6 +89,8 @@ export class IssueRootStore implements IIssueRootStore {
members: string[] | undefined = undefined; members: string[] | undefined = undefined;
projects: string[] | undefined = undefined; projects: string[] | undefined = undefined;
rootStore: RootStore;
issues: IIssueStore; issues: IIssueStore;
state: IStateStore; state: IStateStore;
@ -136,6 +140,8 @@ export class IssueRootStore implements IIssueRootStore {
projects: observable, projects: observable,
}); });
this.rootStore = rootStore;
autorun(() => { autorun(() => {
if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id; if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id;
if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug; if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug;