diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py
index cc9588ca6..713835033 100644
--- a/apiserver/plane/bgtasks/email_notification_task.py
+++ b/apiserver/plane/bgtasks/email_notification_task.py
@@ -186,7 +186,7 @@ def send_email_notification(
}
)
- summary = "updates were made to the issue by"
+ summary = "Updates were made to the issue by"
# Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html
index bdc6a53a3..fa50631c5 100644
--- a/apiserver/templates/emails/notifications/issue-updates.html
+++ b/apiserver/templates/emails/notifications/issue-updates.html
@@ -108,14 +108,33 @@
margin-bottom: 15px;
"
/>
- {% if actors_involved > 0 %}
+ {% if actors_involved == 1 %}
+
+ {{summary}}
+
+ {{ data.0.actor_detail.first_name}}
+ {{data.0.actor_detail.last_name}}
+ .
+
+ {% else %}
+
+ {{summary}}
+
+ {{ data.0.actor_detail.first_name}}
+ {{data.0.actor_detail.last_name }}
+ and others.
+
+ {% endif %}
+
+
+
{% for update in data %} {% if update.changes.name %}
diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule.tsx b/packages/editor/core/src/ui/extensions/horizontal-rule.tsx
deleted file mode 100644
index cee0ded83..000000000
--- a/packages/editor/core/src/ui/extensions/horizontal-rule.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { TextSelection } from "prosemirror-state";
-
-import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
-
-/**
- * Extension based on:
- * - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
- */
-
-export interface HorizontalRuleOptions {
- HTMLAttributes: Record;
-}
-
-declare module "@tiptap/core" {
- interface Commands {
- horizontalRule: {
- /**
- * Add a horizontal rule
- */
- setHorizontalRule: () => ReturnType;
- };
- }
-}
-
-export const HorizontalRule = Node.create({
- name: "horizontalRule",
-
- addOptions() {
- return {
- HTMLAttributes: {},
- };
- },
-
- group: "block",
-
- addAttributes() {
- return {
- color: {
- default: "#dddddd",
- },
- };
- },
-
- parseHTML() {
- return [
- {
- tag: `div[data-type="${this.name}"]`,
- },
- ];
- },
-
- renderHTML({ HTMLAttributes }) {
- return [
- "div",
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
- "data-type": this.name,
- }),
- ["div", {}],
- ];
- },
-
- addCommands() {
- return {
- setHorizontalRule:
- () =>
- ({ chain }) => {
- return (
- chain()
- .insertContent({ type: this.name })
- // set cursor after horizontal rule
- .command(({ tr, dispatch }) => {
- if (dispatch) {
- const { $to } = tr.selection;
- const posAfter = $to.end();
-
- if ($to.nodeAfter) {
- tr.setSelection(TextSelection.create(tr.doc, $to.pos));
- } else {
- // add node after horizontal rule if it’s the end of the document
- const node = $to.parent.type.contentMatch.defaultType?.create();
-
- if (node) {
- tr.insert(posAfter, node);
- tr.setSelection(TextSelection.create(tr.doc, posAfter));
- }
- }
-
- tr.scrollIntoView();
- }
-
- return true;
- })
- .run()
- );
- },
- };
- },
-
- addInputRules() {
- return [
- new InputRule({
- find: /^(?:---|—-|___\s|\*\*\*\s)$/,
- handler: ({ state, range, match }) => {
- state.tr.replaceRangeWith(range.from, range.to, this.type.create());
- },
- }),
- ];
- },
-});
diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx
index 19d8ce894..5bfba3b0f 100644
--- a/packages/editor/core/src/ui/extensions/index.tsx
+++ b/packages/editor/core/src/ui/extensions/index.tsx
@@ -1,26 +1,25 @@
-import StarterKit from "@tiptap/starter-kit";
-import TiptapUnderline from "@tiptap/extension-underline";
-import TextStyle from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
+import TextStyle from "@tiptap/extension-text-style";
+import TiptapUnderline from "@tiptap/extension-underline";
+import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
-import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
+import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
-import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ImageExtension } from "src/ui/extensions/image";
import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions";
-import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
-import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
+import { CustomKeymap } from "src/ui/extensions/keymap";
+import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion";
@@ -55,7 +54,9 @@ export const CoreEditorExtensions = (
},
code: false,
codeBlock: false,
- horizontalRule: false,
+ horizontalRule: {
+ HTMLAttributes: { class: "mt-4 mb-4" },
+ },
blockquote: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
@@ -104,7 +105,6 @@ export const CoreEditorExtensions = (
transformCopiedText: true,
transformPastedText: true,
}),
- HorizontalRule,
Table,
TableHeader,
TableCell,
diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx
index 6a47d79f0..e723ca0d7 100644
--- a/packages/editor/core/src/ui/mentions/custom.tsx
+++ b/packages/editor/core/src/ui/mentions/custom.tsx
@@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions {
}
export const CustomMention = Mention.extend({
+ addStorage(this) {
+ return {
+ mentionsOpen: false,
+ };
+ },
addAttributes() {
return {
id: {
diff --git a/packages/editor/core/src/ui/mentions/suggestion.ts b/packages/editor/core/src/ui/mentions/suggestion.ts
index 6d706cb79..40e75a1e3 100644
--- a/packages/editor/core/src/ui/mentions/suggestion.ts
+++ b/packages/editor/core/src/ui/mentions/suggestion.ts
@@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
+ props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
@@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return true;
}
- // @ts-ignore
- return reactRenderer?.ref?.onKeyDown(props);
+ const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
+
+ if (navigationKeys.includes(props.event.key)) {
+ // @ts-ignore
+ reactRenderer?.ref?.onKeyDown(props);
+ event?.stopPropagation();
+ return true;
+ }
+ return false;
},
- onExit: () => {
+ onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
+ props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
reactRenderer?.destroy();
},
diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx
index b0879d8cd..cf7c4ee18 100644
--- a/packages/editor/core/src/ui/read-only/extensions.tsx
+++ b/packages/editor/core/src/ui/read-only/extensions.tsx
@@ -11,7 +11,6 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
-import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
import { isValidHttpUrl } from "src/lib/utils";
@@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
},
},
codeBlock: false,
- horizontalRule: false,
+ horizontalRule: {
+ HTMLAttributes: { class: "mt-4 mb-4" },
+ },
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
@@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-lg border border-custom-border-300",
},
}),
- HorizontalRule,
TiptapUnderline,
TextStyle,
Color,
diff --git a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx
index 129efa4ee..7d93bf36f 100644
--- a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx
+++ b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx
@@ -4,13 +4,16 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: "enterKey",
- addKeyboardShortcuts() {
+ addKeyboardShortcuts(this) {
return {
Enter: () => {
- if (onEnterKeyPress) {
- onEnterKeyPress();
+ if (!this.editor.storage.mentionsOpen) {
+ if (onEnterKeyPress) {
+ onEnterKeyPress();
+ }
+ return true;
}
- return true;
+ return false;
},
"Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [
diff --git a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx
index 527fd5674..c4b24d166 100644
--- a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx
+++ b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx
@@ -1,5 +1,3 @@
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
-export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
- // EnterKeyExtension(onEnterKeyPress),
-];
+export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
diff --git a/web/components/account/o-auth/google-sign-in.tsx b/web/components/account/o-auth/google-sign-in.tsx
index 93958bbd2..c1c57baa0 100644
--- a/web/components/account/o-auth/google-sign-in.tsx
+++ b/web/components/account/o-auth/google-sign-in.tsx
@@ -31,7 +31,6 @@ export const GoogleSignInButton: FC = (props) => {
size: "large",
logo_alignment: "center",
text: type === "sign_in" ? "signin_with" : "signup_with",
- width: 384,
} as GsiButtonConfiguration // customization attributes
);
} catch (err) {
diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx
index a5ffd807a..b2e4c4c9f 100644
--- a/web/components/core/image-picker-popover.tsx
+++ b/web/components/core/image-picker-popover.tsx
@@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC = observer((props) => {
onChange(unsplashImages[0].urls.regular);
}, [value, onChange, unsplashImages]);
- const openDropdown = () => setIsOpen(true);
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ };
- useOutsideClickDetector(ref, closeDropdown);
+ const toggleDropdown = () => {
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(ref, handleClose);
return (
{label}
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx
index 33c2b3c82..52da51b77 100644
--- a/web/components/cycles/sidebar.tsx
+++ b/web/components/cycles/sidebar.tsx
@@ -587,14 +587,16 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
+ {cycleDetails && cycleDetails.distribution && (
+
+ )}
) : (
""
diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx
index 2076de7fb..af2c11660 100644
--- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx
+++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx
@@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC = (props) => {
})}
>
Issues
-
+
{totalIssues}
diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx
index f8cd2d50a..bd4171cfa 100644
--- a/web/components/dashboard/widgets/issues-by-state-group.tsx
+++ b/web/components/dashboard/widgets/issues-by-state-group.tsx
@@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props)
startedCount > 0
? "started"
: unStartedCount > 0
- ? "unstarted"
- : backlogCount > 0
- ? "backlog"
- : completedCount > 0
- ? "completed"
- : canceledCount > 0
- ? "cancelled"
- : null;
+ ? "unstarted"
+ : backlogCount > 0
+ ? "backlog"
+ : completedCount > 0
+ ? "completed"
+ : canceledCount > 0
+ ? "cancelled"
+ : null;
setActiveStateGroup(stateGroup);
setDefaultStateGroup(stateGroup);
@@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC = observer((props)
/>
{totalCount > 0 ? (
-
-
-
+
+
+
datum.data.color}
@@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props)
layers={["arcs", CenteredMetric]}
/>
-
+
{chartData.map((item) => (
diff --git a/web/components/dropdowns/buttons.tsx b/web/components/dropdowns/buttons.tsx
new file mode 100644
index 000000000..93d8c187c
--- /dev/null
+++ b/web/components/dropdowns/buttons.tsx
@@ -0,0 +1,101 @@
+// helpers
+import { cn } from "helpers/common.helper";
+// types
+import { TButtonVariants } from "./types";
+// constants
+import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
+import { Tooltip } from "@plane/ui";
+
+export type DropdownButtonProps = {
+ children: React.ReactNode;
+ className?: string;
+ isActive: boolean;
+ tooltipContent: string | React.ReactNode;
+ tooltipHeading: string;
+ showTooltip: boolean;
+ variant: TButtonVariants;
+};
+
+type ButtonProps = {
+ children: React.ReactNode;
+ className?: string;
+ isActive: boolean;
+ tooltipContent: string | React.ReactNode;
+ tooltipHeading: string;
+ showTooltip: boolean;
+};
+
+export const DropdownButton: React.FC
= (props) => {
+ const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
+
+ const ButtonToRender: React.FC = BORDER_BUTTON_VARIANTS.includes(variant)
+ ? BorderButton
+ : BACKGROUND_BUTTON_VARIANTS.includes(variant)
+ ? BackgroundButton
+ : TransparentButton;
+
+ return (
+
+ {children}
+
+ );
+};
+
+const BorderButton: React.FC = (props) => {
+ const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const BackgroundButton: React.FC = (props) => {
+ const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const TransparentButton: React.FC = (props) => {
+ const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/web/components/dropdowns/constants.ts b/web/components/dropdowns/constants.ts
new file mode 100644
index 000000000..ce52ad505
--- /dev/null
+++ b/web/components/dropdowns/constants.ts
@@ -0,0 +1,20 @@
+// types
+import { TButtonVariants } from "./types";
+
+export const BORDER_BUTTON_VARIANTS: TButtonVariants[] = ["border-with-text", "border-without-text"];
+
+export const BACKGROUND_BUTTON_VARIANTS: TButtonVariants[] = ["background-with-text", "background-without-text"];
+
+export const TRANSPARENT_BUTTON_VARIANTS: TButtonVariants[] = ["transparent-with-text", "transparent-without-text"];
+
+export const BUTTON_VARIANTS_WITHOUT_TEXT: TButtonVariants[] = [
+ "border-without-text",
+ "background-without-text",
+ "transparent-without-text",
+];
+
+export const BUTTON_VARIANTS_WITH_TEXT: TButtonVariants[] = [
+ "border-with-text",
+ "background-with-text",
+ "transparent-with-text",
+];
diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx
index 39b72fe08..d6d4da432 100644
--- a/web/components/dropdowns/cycle.tsx
+++ b/web/components/dropdowns/cycle.tsx
@@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useCycle } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
+// components
+import { DropdownButton } from "./buttons";
// icons
-import { ContrastIcon, Tooltip } from "@plane/ui";
+import { ContrastIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
-import { ICycle } from "@plane/types";
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,18 +27,6 @@ type Props = TDropdownProps & {
value: string | null;
};
-type ButtonProps = {
- className?: string;
- cycle: ICycle | null;
- hideIcon: boolean;
- hideText?: boolean;
- dropdownArrow: boolean;
- isActive?: boolean;
- dropdownArrowClassName: string;
- placeholder: string;
- tooltip: boolean;
-};
-
type DropdownOptions =
| {
value: string | null;
@@ -44,100 +35,6 @@ type DropdownOptions =
}[]
| undefined;
-const BorderButton = (props: ButtonProps) => {
- const {
- className,
- cycle,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }{" "}
- {!hideText && {cycle?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- cycle,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && {cycle?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- cycle,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && {cycle?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
export const CycleDropdown: React.FC = observer((props) => {
const {
button,
@@ -153,8 +50,8 @@ export const CycleDropdown: React.FC = observer((props) => {
placeholder = "Cycle",
placement,
projectId,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -221,13 +118,34 @@ export const CycleDropdown: React.FC = observer((props) => {
const selectedCycle = value ? getCycleById(value) : null;
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string | null) => {
+ onChange(val);
+ handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
- onChange={onChange}
+ onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -246,7 +164,7 @@ export const CycleDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -262,77 +180,24 @@ export const CycleDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {/* TODO: move button components to a single file for each dropdown */}
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && }
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+ {selectedCycle?.name ?? placeholder}
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
@@ -366,7 +231,6 @@ export const CycleDropdown: React.FC = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx
index 5dac4ee49..1dba6f780 100644
--- a/web/components/dropdowns/date.tsx
+++ b/web/components/dropdowns/date.tsx
@@ -2,17 +2,19 @@ import React, { useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import DatePicker from "react-datepicker";
import { usePopper } from "react-popper";
-import { Calendar, CalendarDays, X } from "lucide-react";
+import { CalendarDays, X } from "lucide-react";
// hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
-// ui
-import { Tooltip } from "@plane/ui";
+// components
+import { DropdownButton } from "./buttons";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
clearIconClassName?: string;
@@ -23,157 +25,6 @@ type Props = TDropdownProps & {
onChange: (val: Date | null) => void;
value: Date | string | null;
closeOnSelect?: boolean;
- showPlaceholderIcon?: boolean;
-};
-
-type ButtonProps = {
- className?: string;
- clearIconClassName: string;
- date: string | Date | null;
- icon: React.ReactNode;
- isClearable: boolean;
- hideIcon?: boolean;
- hideText?: boolean;
- isActive?: boolean;
- onClear: () => void;
- placeholder: string;
- tooltip: boolean;
- showPlaceholderIcon?: boolean;
-};
-
-const BorderButton = (props: ButtonProps) => {
- const {
- className,
- clearIconClassName,
- date,
- icon,
- isClearable,
- hideIcon = false,
- hideText = false,
- isActive = false,
- onClear,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && icon}
- {!hideText && {date ? renderFormattedDate(date) : placeholder}}
- {isClearable && (
- {
- e.stopPropagation();
- onClear();
- }}
- />
- )}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- clearIconClassName,
- date,
- icon,
- isClearable,
- hideIcon = false,
- hideText = false,
- onClear,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && icon}
- {!hideText && {date ? renderFormattedDate(date) : placeholder}}
- {isClearable && (
- {
- e.stopPropagation();
- onClear();
- }}
- />
- )}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- clearIconClassName,
- date,
- icon,
- isClearable,
- hideIcon = false,
- hideText = false,
- isActive = false,
- onClear,
- placeholder,
- tooltip,
- showPlaceholderIcon = false,
- } = props;
-
- return (
-
-
- {!hideIcon && icon}
- {!hideText && {date ? renderFormattedDate(date) : placeholder}}
- {showPlaceholderIcon && !date && (
-
- )}
-
- {isClearable && (
- {
- e.stopPropagation();
- onClear();
- }}
- />
- )}
-
-
- );
};
export const DateDropdown: React.FC = (props) => {
@@ -193,9 +44,8 @@ export const DateDropdown: React.FC = (props) => {
onChange,
placeholder = "Date",
placement,
+ showTooltip = false,
tabIndex,
- tooltip = false,
- showPlaceholderIcon = false,
value,
} = props;
const [isOpen, setIsOpen] = useState(false);
@@ -217,15 +67,36 @@ export const DateDropdown: React.FC = (props) => {
],
});
- const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== "";
+ const isDateSelected = value && value.toString().trim() !== "";
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: Date | null) => {
+ onChange(val);
+ if (closeOnSelect) handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= (props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
- onChange(null)}
- isActive={isOpen}
- tooltip={tooltip}
- />
- ) : buttonVariant === "border-without-text" ? (
- onChange(null)}
- isActive={isOpen}
- tooltip={tooltip}
- hideText
- />
- ) : buttonVariant === "background-with-text" ? (
- onChange(null)}
- tooltip={tooltip}
- />
- ) : buttonVariant === "background-without-text" ? (
- onChange(null)}
- tooltip={tooltip}
- hideText
- />
- ) : buttonVariant === "transparent-with-text" ? (
- onChange(null)}
- isActive={isOpen}
- tooltip={tooltip}
- showPlaceholderIcon={showPlaceholderIcon}
- />
- ) : buttonVariant === "transparent-without-text" ? (
- onChange(null)}
- isActive={isOpen}
- tooltip={tooltip}
- hideText
- showPlaceholderIcon={showPlaceholderIcon}
- />
- ) : null}
+
+ {!hideIcon && icon}
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+ {value ? renderFormattedDate(value) : placeholder}
+ )}
+ {isClearable && isDateSelected && (
+ {
+ e.stopPropagation();
+ onChange(null);
+ }}
+ />
+ )}
+
{isOpen && (
@@ -338,10 +149,7 @@ export const DateDropdown: React.FC = (props) => {
{
- onChange(val);
- if (closeOnSelect) closeDropdown();
- }}
+ onChange={dropdownOnChange}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx
index 31b2a840d..88fec3199 100644
--- a/web/components/dropdowns/estimate.tsx
+++ b/web/components/dropdowns/estimate.tsx
@@ -8,12 +8,14 @@ import sortBy from "lodash/sortBy";
import { useApplication, useEstimate } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
-// ui
-import { Tooltip } from "@plane/ui";
+// components
+import { DropdownButton } from "./buttons";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,18 +26,6 @@ type Props = TDropdownProps & {
value: number | null;
};
-type ButtonProps = {
- className?: string;
- estimatePoint: string | null;
- dropdownArrow: boolean;
- dropdownArrowClassName: string;
- hideIcon?: boolean;
- hideText?: boolean;
- isActive?: boolean;
- placeholder: string;
- tooltip: boolean;
-};
-
type DropdownOptions =
| {
value: number | null;
@@ -44,118 +34,6 @@ type DropdownOptions =
}[]
| undefined;
-const BorderButton = (props: ButtonProps) => {
- const {
- className,
- estimatePoint,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
- {estimatePoint !== null ? estimatePoint : placeholder}
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- estimatePoint,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
- {estimatePoint !== null ? estimatePoint : placeholder}
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- estimatePoint,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
- {estimatePoint !== null ? estimatePoint : placeholder}
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
export const EstimateDropdown: React.FC = observer((props) => {
const {
button,
@@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC = observer((props) => {
placeholder = "Estimate",
placement,
projectId,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC = observer((props) => {
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
- const openDropdown = () => {
- setIsOpen(true);
-
+ const onOpen = () => {
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: number | null) => {
+ onChange(val);
+ handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
tabIndex={tabIndex}
className={cn("h-full w-full", className)}
value={value}
- onChange={onChange}
+ onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -255,7 +153,7 @@ export const EstimateDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && }
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+ {selectedEstimate !== null ? selectedEstimate : placeholder}
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
@@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/index.ts b/web/components/dropdowns/index.ts
index 53be7e4f5..036ed9f75 100644
--- a/web/components/dropdowns/index.ts
+++ b/web/components/dropdowns/index.ts
@@ -3,7 +3,6 @@ export * from "./cycle";
export * from "./date";
export * from "./estimate";
export * from "./module";
-export * from "./module-select";
export * from "./priority";
export * from "./project";
export * from "./state";
diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx
new file mode 100644
index 000000000..067d609c5
--- /dev/null
+++ b/web/components/dropdowns/member/avatar.tsx
@@ -0,0 +1,37 @@
+import { observer } from "mobx-react-lite";
+// hooks
+import { useMember } from "hooks/store";
+// ui
+import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
+
+type AvatarProps = {
+ showTooltip: boolean;
+ userIds: string | string[] | null;
+};
+
+export const ButtonAvatars: React.FC = observer((props) => {
+ const { showTooltip, userIds } = props;
+ // store hooks
+ const { getUserDetails } = useMember();
+
+ if (Array.isArray(userIds)) {
+ if (userIds.length > 0)
+ return (
+
+ {userIds.map((userId) => {
+ const userDetails = getUserDetails(userId);
+
+ if (!userDetails) return;
+ return ;
+ })}
+
+ );
+ } else {
+ if (userIds) {
+ const userDetails = getUserDetails(userIds);
+ return ;
+ }
+ }
+
+ return ;
+});
diff --git a/web/components/dropdowns/member/buttons.tsx b/web/components/dropdowns/member/buttons.tsx
deleted file mode 100644
index e1664cdb4..000000000
--- a/web/components/dropdowns/member/buttons.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-import { observer } from "mobx-react-lite";
-import { ChevronDown } from "lucide-react";
-// hooks
-import { useMember } from "hooks/store";
-// ui
-import { Avatar, AvatarGroup, Tooltip, UserGroupIcon } from "@plane/ui";
-// helpers
-import { cn } from "helpers/common.helper";
-
-type ButtonProps = {
- className?: string;
- dropdownArrow: boolean;
- dropdownArrowClassName: string;
- placeholder: string;
- hideIcon?: boolean;
- hideText?: boolean;
- isActive?: boolean;
- tooltip: boolean;
- userIds: string | string[] | null;
-};
-
-const ButtonAvatars = observer(({ tooltip, userIds }: { tooltip: boolean; userIds: string | string[] | null }) => {
- const { getUserDetails } = useMember();
-
- if (Array.isArray(userIds)) {
- if (userIds.length > 0)
- return (
-
- {userIds.map((userId) => {
- const userDetails = getUserDetails(userId);
-
- if (!userDetails) return;
- return ;
- })}
-
- );
- } else {
- if (userIds) {
- const userDetails = getUserDetails(userIds);
- return ;
- }
- }
-
- return ;
-});
-
-export const BorderButton = observer((props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- userIds,
- tooltip,
- } = props;
- // store hooks
- const { getUserDetails } = useMember();
-
- const isArray = Array.isArray(userIds);
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
-
- {isArray && userIds.length > 0
- ? userIds.length === 1
- ? getUserDetails(userIds[0])?.display_name
- : ""
- : placeholder}
-
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-});
-
-export const BackgroundButton = observer((props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- userIds,
- tooltip,
- } = props;
- // store hooks
- const { getUserDetails } = useMember();
-
- const isArray = Array.isArray(userIds);
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
-
- {isArray && userIds.length > 0
- ? userIds.length === 1
- ? getUserDetails(userIds[0])?.display_name
- : ""
- : placeholder}
-
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-});
-
-export const TransparentButton = observer((props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- placeholder,
- userIds,
- tooltip,
- } = props;
- // store hooks
- const { getUserDetails } = useMember();
-
- const isArray = Array.isArray(userIds);
-
- return (
-
-
- {!hideIcon && }
- {!hideText && (
-
- {isArray && userIds.length > 0
- ? userIds.length === 1
- ? getUserDetails(userIds[0])?.display_name
- : ""
- : placeholder}
-
- )}
- {dropdownArrow && (
-
- )}
-
-
- );
-});
diff --git a/web/components/dropdowns/member/index.ts b/web/components/dropdowns/member/index.ts
index bc976b46a..a9f7e09c8 100644
--- a/web/components/dropdowns/member/index.ts
+++ b/web/components/dropdowns/member/index.ts
@@ -1,3 +1,2 @@
-export * from "./buttons";
export * from "./project-member";
export * from "./workspace-member";
diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx
index cc1650527..cfbdf52e6 100644
--- a/web/components/dropdowns/member/project-member.tsx
+++ b/web/components/dropdowns/member/project-member.tsx
@@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
-import { Check, Search } from "lucide-react";
+import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
-import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
+import { ButtonAvatars } from "./avatar";
+import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
type Props = {
projectId: string;
@@ -36,8 +39,8 @@ export const ProjectMemberDropdown: React.FC = observer((props) => {
placeholder = "Members",
placement,
projectId,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC = observer((props) => {
};
if (multiple) comboboxProps.multiple = true;
- const openDropdown = () => {
- setIsOpen(true);
-
+ const onOpen = () => {
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string & string[]) => {
+ onChange(val);
+ if (!multiple) handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
+ onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
@@ -121,7 +145,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && }
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+
+ {Array.isArray(value) && value.length > 0
+ ? value.length === 1
+ ? getUserDetails(value[0])?.display_name
+ : ""
+ : placeholder}
+
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
@@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={() => {
- if (!multiple) closeDropdown();
- }}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx
index aaa1d7f7e..980f344a6 100644
--- a/web/components/dropdowns/member/workspace-member.tsx
+++ b/web/components/dropdowns/member/workspace-member.tsx
@@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
-import { Check, Search } from "lucide-react";
+import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
-import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
+import { ButtonAvatars } from "./avatar";
+import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
export const WorkspaceMemberDropdown: React.FC = observer((props) => {
const {
@@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC = observer((
onChange,
placeholder = "Members",
placement,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
const [query, setQuery] = useState("");
- const [isOpen, setIsOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef(null);
// popper-js refs
@@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC = observer((
};
if (multiple) comboboxProps.multiple = true;
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string & string[]) => {
+ onChange(val);
+ if (!multiple) handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((
className={cn("h-full", className)}
{...comboboxProps}
handleKeyDown={handleKeyDown}
+ onChange={dropdownOnChange}
>
{button ? (
@@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC = observer((
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
+ onClick={handleOnClick}
>
{button}
@@ -125,124 +151,82 @@ export const WorkspaceMemberDropdown: React.FC = observer((
},
buttonContainerClassName
)}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && }
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+
+ {Array.isArray(value) && value.length > 0
+ ? value.length === 1
+ ? getUserDetails(value[0])?.display_name
+ : ""
+ : placeholder}
+
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
-
-
-
-
- setQuery(e.target.value)}
- placeholder="Search"
- displayValue={(assigned: any) => assigned?.name}
- />
-
-
- {filteredOptions ? (
- filteredOptions.length > 0 ? (
- filteredOptions.map((option) => (
-
- `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
- active ? "bg-custom-background-80" : ""
- } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
- }
- onClick={() => {
- if (!multiple) closeDropdown();
- }}
- >
- {({ selected }) => (
- <>
- {option.content}
- {selected && }
- >
- )}
-
- ))
+ {isOpen && (
+
+
+
+
+ setQuery(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
+ active ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+ <>
+ {option.content}
+ {selected && }
+ >
+ )}
+
+ ))
+ ) : (
+
No matching results
+ )
) : (
-
No matching results
- )
- ) : (
-
Loading...
- )}
+
Loading...
+ )}
+
-
-
+
+ )}
);
});
diff --git a/web/components/dropdowns/module-select/button.tsx b/web/components/dropdowns/module-select/button.tsx
deleted file mode 100644
index 85c97d449..000000000
--- a/web/components/dropdowns/module-select/button.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { FC } from "react";
-import { twMerge } from "tailwind-merge";
-import { observer } from "mobx-react-lite";
-import { ChevronDown, X } from "lucide-react";
-// hooks
-import { useModule } from "hooks/store";
-// ui and components
-import { DiceIcon, Tooltip } from "@plane/ui";
-// types
-import { TModuleSelectButton } from "./types";
-
-export const ModuleSelectButton: FC
= observer((props) => {
- const {
- value,
- onChange,
- placeholder,
- buttonClassName,
- buttonVariant,
- hideIcon,
- hideText,
- dropdownArrow,
- dropdownArrowClassName,
- showTooltip,
- showCount,
- } = props;
- // hooks
- const { getModuleById } = useModule();
-
- return (
-
-
- {value && typeof value === "string" ? (
-
- {!hideIcon && }
- {!hideText && (
-
- {getModuleById(value)?.name || placeholder}
-
- )}
-
- ) : value && Array.isArray(value) && value.length > 0 ? (
- showCount ? (
-
- {!hideIcon && }
- {!hideText && (
-
- {value.length} Modules
-
- )}
-
- ) : (
- value.map((moduleId) => {
- const _module = getModuleById(moduleId);
- if (!_module) return <>>;
- return (
-
-
-
- {!hideIcon && }
- {!hideText && (
- {_module?.name}
- )}
-
-
-
- {
- e.preventDefault();
- e.stopPropagation();
- onChange(_module.id);
- }}
- >
-
-
-
-
- );
- })
- )
- ) : (
- !hideText && (
-
- {!hideIcon && }
- {!hideText && (
-
- {placeholder}
-
- )}
-
- )
- )}
-
-
- {dropdownArrow && (
-
- )}
-
- );
-});
diff --git a/web/components/dropdowns/module-select/index.ts b/web/components/dropdowns/module-select/index.ts
deleted file mode 100644
index 2161534fb..000000000
--- a/web/components/dropdowns/module-select/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./button";
-export * from "./select";
diff --git a/web/components/dropdowns/module-select/select.tsx b/web/components/dropdowns/module-select/select.tsx
deleted file mode 100644
index a8ddfccf7..000000000
--- a/web/components/dropdowns/module-select/select.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { FC, useEffect, useRef, useState, Fragment } from "react";
-import { observer } from "mobx-react-lite";
-import { Combobox } from "@headlessui/react";
-import { usePopper } from "react-popper";
-import { Check, Search } from "lucide-react";
-import { twMerge } from "tailwind-merge";
-// hooks
-import { useModule } from "hooks/store";
-import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
-import useOutsideClickDetector from "hooks/use-outside-click-detector";
-// components
-import { ModuleSelectButton } from "./";
-// types
-import { TModuleSelectDropdown, TModuleSelectDropdownOption } from "./types";
-import { DiceIcon } from "@plane/ui";
-
-export const ModuleSelectDropdown: FC = observer((props) => {
- // props
- const {
- workspaceSlug,
- projectId,
- value = undefined,
- onChange,
- placeholder = "Module",
- multiple = false,
- disabled = false,
- className = "",
- buttonContainerClassName = "",
- buttonClassName = "",
- buttonVariant = "transparent-with-text",
- hideIcon = false,
- dropdownArrow = false,
- dropdownArrowClassName = "",
- showTooltip = false,
- showCount = false,
- placement,
- tabIndex,
- button,
- } = props;
- // states
- const [query, setQuery] = useState("");
- const [isOpen, setIsOpen] = useState(false);
- // refs
- const dropdownRef = useRef(null);
- // popper-js refs
- const [referenceElement, setReferenceElement] = useState(null);
- const [popperElement, setPopperElement] = useState(null);
- // popper-js init
- const { styles, attributes } = usePopper(referenceElement, popperElement, {
- placement: placement ?? "bottom-start",
- modifiers: [
- {
- name: "preventOverflow",
- options: {
- padding: 12,
- },
- },
- ],
- });
- // store hooks
- const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
-
- const moduleIds = getProjectModuleIds(projectId);
-
- const options: TModuleSelectDropdownOption[] | undefined = moduleIds?.map((moduleId) => {
- const moduleDetails = getModuleById(moduleId);
- return {
- value: moduleId,
- query: `${moduleDetails?.name}`,
- content: (
-
-
- {moduleDetails?.name}
-
- ),
- };
- });
- !multiple &&
- options?.unshift({
- value: undefined,
- query: "No module",
- content: (
-
-
- No module
-
- ),
- });
-
- const filteredOptions =
- query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
-
- // fetch modules of the project if not already present in the store
- useEffect(() => {
- if (!workspaceSlug) return;
-
- if (!moduleIds) fetchModules(workspaceSlug, projectId);
- }, [moduleIds, fetchModules, projectId, workspaceSlug]);
-
- const openDropdown = () => {
- if (isOpen) closeDropdown();
- else {
- setIsOpen(true);
- if (referenceElement) referenceElement.focus();
- }
- };
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
-
- const comboboxProps: any = {};
- if (multiple) comboboxProps.multiple = true;
-
- return (
-
-
- {button ? (
-
- ) : (
-
- )}
-
- {isOpen && (
-
-
-
-
- setQuery(e.target.value)}
- placeholder="Search"
- displayValue={(moduleIds: any) => {
- const displayValueOptions: TModuleSelectDropdownOption[] | undefined = options?.filter((_module) =>
- moduleIds.includes(_module.value)
- );
- return displayValueOptions?.map((_option) => _option.query).join(", ") || "Select Module";
- }}
- />
-
-
- {filteredOptions ? (
- filteredOptions.length > 0 ? (
- filteredOptions.map((option) => (
-
- `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
- active ? "bg-custom-background-80" : ""
- } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
- }
- onClick={() => !multiple && closeDropdown()}
- >
- {({ selected }) => (
- <>
- {option.content}
- {selected && }
- >
- )}
-
- ))
- ) : (
-
No matching results
- )
- ) : (
-
Loading...
- )}
-
-
-
- )}
-
- );
-});
diff --git a/web/components/dropdowns/module-select/types.d.ts b/web/components/dropdowns/module-select/types.d.ts
deleted file mode 100644
index b1c10eedb..000000000
--- a/web/components/dropdowns/module-select/types.d.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { ReactNode } from "react";
-import { Placement } from "@popperjs/core";
-import { TDropdownProps, TButtonVariants } from "../types";
-
-type TModuleSelectDropdownRoot = Omit<
- TDropdownProps,
- "buttonClassName",
- "buttonContainerClassName",
- "buttonContainerClassName",
- "className",
- "disabled",
- "hideIcon",
- "placeholder",
- "placement",
- "tabIndex",
- "tooltip"
->;
-
-export type TModuleSelectDropdownBase = {
- value: string | string[] | undefined;
- onChange: (moduleIds: undefined | string | (string | undefined)[]) => void;
- placeholder?: string;
- disabled?: boolean;
- buttonClassName?: string;
- buttonVariant?: TButtonVariants;
- hideIcon?: boolean;
- dropdownArrow?: boolean;
- dropdownArrowClassName?: string;
- showTooltip?: boolean;
- showCount?: boolean;
-};
-
-export type TModuleSelectButton = TModuleSelectDropdownBase & { hideText?: boolean };
-
-export type TModuleSelectDropdown = TModuleSelectDropdownBase & {
- workspaceSlug: string;
- projectId: string;
- multiple?: boolean;
- className?: string;
- buttonContainerClassName?: string;
- placement?: Placement;
- tabIndex?: number;
- button?: ReactNode;
-};
-
-export type TModuleSelectDropdownOption = {
- value: string | undefined;
- query: string;
- content: JSX.Element;
-};
diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx
index 4fc5e0e22..e673293e0 100644
--- a/web/components/dropdowns/module.tsx
+++ b/web/components/dropdowns/module.tsx
@@ -2,27 +2,40 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
-import { Check, ChevronDown, Search } from "lucide-react";
+import { Check, ChevronDown, Search, X } from "lucide-react";
// hooks
import { useApplication, useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
+// components
+import { DropdownButton } from "./buttons";
// icons
import { DiceIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
-import { IModule } from "@plane/types";
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
- onChange: (val: string | null) => void;
projectId: string;
- value: string | null;
-};
+ showCount?: boolean;
+} & (
+ | {
+ multiple: false;
+ onChange: (val: string | null) => void;
+ value: string | null;
+ }
+ | {
+ multiple: true;
+ onChange: (val: string[]) => void;
+ value: string[];
+ }
+ );
type DropdownOptions =
| {
@@ -32,110 +45,97 @@ type DropdownOptions =
}[]
| undefined;
-type ButtonProps = {
- className?: string;
+type ButtonContentProps = {
+ disabled: boolean;
dropdownArrow: boolean;
dropdownArrowClassName: string;
- hideIcon?: boolean;
- hideText?: boolean;
- isActive?: boolean;
- module: IModule | null;
+ hideIcon: boolean;
+ hideText: boolean;
+ onChange: (moduleIds: string[]) => void;
placeholder: string;
- tooltip: boolean;
+ showCount: boolean;
+ value: string | string[] | null;
};
-const BorderButton = (props: ButtonProps) => {
+const ButtonContent: React.FC = (props) => {
const {
- className,
+ disabled,
dropdownArrow,
dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- module,
+ hideIcon,
+ hideText,
+ onChange,
placeholder,
- tooltip,
+ showCount,
+ value,
} = props;
+ // store hooks
+ const { getModuleById } = useModule();
- return (
-
-
+ {showCount ? (
+ <>
+ {!hideIcon &&
}
+
+ {value.length > 0 ? `${value.length} Module${value.length === 1 ? "" : "s"}` : placeholder}
+
+ >
+ ) : value.length > 0 ? (
+
+ {value.map((moduleId) => {
+ const moduleDetails = getModuleById(moduleId);
+ return (
+
+ {!hideIcon && }
+ {!hideText && (
+
+ {moduleDetails?.name}
+
+ )}
+ {!disabled && (
+
+
+
+ )}
+
+ );
+ })}
+
+ ) : (
+ <>
+ {!hideIcon &&
}
+
{placeholder}
+ >
)}
- >
- {!hideIcon &&
}
- {!hideText &&
{module?.name ?? placeholder}}
{dropdownArrow && (
)}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- module,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
+ >
+ );
+ else
+ return (
+ <>
{!hideIcon && }
- {!hideText && {module?.name ?? placeholder}}
+ {!hideText && {value ?? placeholder}}
{dropdownArrow && (
)}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- module,
- placeholder,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && }
- {!hideText && {module?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
+ >
+ );
};
export const ModuleDropdown: React.FC = observer((props) => {
@@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC = observer((props) => {
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
+ multiple,
onChange,
placeholder = "Module",
placement,
projectId,
+ showCount = false,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -186,7 +188,6 @@ export const ModuleDropdown: React.FC = observer((props) => {
const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
-
return {
value: moduleId,
query: `${moduleDetails?.name}`,
@@ -198,16 +199,17 @@ export const ModuleDropdown: React.FC = observer((props) => {
),
};
});
- options?.unshift({
- value: null,
- query: "No module",
- content: (
-
-
- No module
-
- ),
- });
+ if (!multiple)
+ options?.unshift({
+ value: null,
+ query: "No module",
+ content: (
+
+
+ No module
+
+ ),
+ });
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
@@ -219,15 +221,41 @@ export const ModuleDropdown: React.FC = observer((props) => {
if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
- const selectedModule = value ? getModuleById(value) : null;
-
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string & string[]) => {
+ onChange(val);
+ if (!multiple) handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
+
+ const comboboxProps: any = {
+ value,
+ onChange: dropdownOnChange,
+ disabled,
+ };
+ if (multiple) comboboxProps.multiple = true;
return (
= observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
- value={value}
- onChange={onChange}
- disabled={disabled}
onKeyDown={handleKeyDown}
+ {...comboboxProps}
>
{button ? (
@@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
+
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
)}
@@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC = observer((props) => {
key={option.value}
value={option.value}
className={({ active, selected }) =>
- `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
- active ? "bg-custom-background-80" : ""
- } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ cn(
+ "w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
+ {
+ "bg-custom-background-80": active,
+ "text-custom-text-100": selected,
+ "text-custom-text-200": !selected,
+ }
+ )
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx
index 7f96a3bd1..f620e6818 100644
--- a/web/components/dropdowns/priority.tsx
+++ b/web/components/dropdowns/priority.tsx
@@ -15,6 +15,7 @@ import { TIssuePriorities } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
+import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -34,7 +35,7 @@ type ButtonProps = {
isActive?: boolean;
highlightUrgent: boolean;
priority: TIssuePriorities;
- tooltip: boolean;
+ showTooltip: boolean;
};
const BorderButton = (props: ButtonProps) => {
@@ -46,7 +47,7 @@ const BorderButton = (props: ButtonProps) => {
hideText = false,
highlightUrgent,
priority,
- tooltip,
+ showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -60,7 +61,7 @@ const BorderButton = (props: ButtonProps) => {
};
return (
-
+
{
hideText = false,
highlightUrgent,
priority,
- tooltip,
+ showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -129,7 +130,7 @@ const BackgroundButton = (props: ButtonProps) => {
};
return (
-
+
{
isActive = false,
highlightUrgent,
priority,
- tooltip,
+ showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -199,7 +200,7 @@ const TransparentButton = (props: ButtonProps) => {
};
return (
-
+
= (props) => {
highlightUrgent = true,
onChange,
placement,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC
= (props) => {
const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: TIssuePriorities) => {
+ onChange(val);
+ handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
+
+ const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
+ ? BorderButton
+ : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant)
+ ? BackgroundButton
+ : TransparentButton;
return (
= (props) => {
className
)}
value={value}
- onChange={onChange}
+ onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC = (props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -349,86 +377,20 @@ export const PriorityDropdown: React.FC = (props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
)}
@@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC = (props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx
index cdbe21b08..719308a65 100644
--- a/web/components/dropdowns/project.tsx
+++ b/web/components/dropdowns/project.tsx
@@ -7,14 +7,15 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useProject } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
-// ui
-import { Tooltip } from "@plane/ui";
+// components
+import { DropdownButton } from "./buttons";
// helpers
import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
-import { IProject } from "@plane/types";
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,119 +25,6 @@ type Props = TDropdownProps & {
value: string | null;
};
-type ButtonProps = {
- className?: string;
- dropdownArrow: boolean;
- dropdownArrowClassName: string;
- hideIcon?: boolean;
- hideText?: boolean;
- placeholder: string;
- project: IProject | null;
- tooltip: boolean;
-};
-
-const BorderButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- project,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
-
- )}
- {!hideText && {project?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- project,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
-
- )}
- {!hideText && {project?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- placeholder,
- project,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
-
- )}
- {!hideText && {project?.name ?? placeholder}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
export const ProjectDropdown: React.FC = observer((props) => {
const {
button,
@@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC = observer((props) => {
onChange,
placeholder = "Project",
placement,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC = observer((props) => {
const selectedProject = value ? getProjectById(value) : null;
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string) => {
+ onChange(val);
+ handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
- onChange={onChange}
+ onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -229,7 +138,7 @@ export const ProjectDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && (
+
+ {selectedProject?.emoji
+ ? renderEmoji(selectedProject?.emoji)
+ : selectedProject?.icon_prop
+ ? renderEmoji(selectedProject?.icon_prop)
+ : null}
+
+ )}
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+ {selectedProject?.name ?? placeholder}
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
@@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx
index 9ad41622b..e92dd40a0 100644
--- a/web/components/dropdowns/state.tsx
+++ b/web/components/dropdowns/state.tsx
@@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useProjectState } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
+// components
+import { DropdownButton } from "./buttons";
// icons
-import { StateGroupIcon, Tooltip } from "@plane/ui";
+import { StateGroupIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
-import { IState } from "@plane/types";
import { TDropdownProps } from "./types";
+// constants
+import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,130 +27,6 @@ type Props = TDropdownProps & {
value: string;
};
-type ButtonProps = {
- className?: string;
- dropdownArrow: boolean;
- dropdownArrowClassName: string;
- hideIcon?: boolean;
- hideText?: boolean;
- isActive?: boolean;
- state: IState | undefined;
- tooltip: boolean;
-};
-
-const BorderButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- state,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- )}
- {!hideText && {state?.name ?? "State"}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const BackgroundButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- state,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- )}
- {!hideText && {state?.name ?? "State"}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
-const TransparentButton = (props: ButtonProps) => {
- const {
- className,
- dropdownArrow,
- dropdownArrowClassName,
- hideIcon = false,
- hideText = false,
- isActive = false,
- state,
- tooltip,
- } = props;
-
- return (
-
-
- {!hideIcon && (
-
- )}
- {!hideText && {state?.name ?? "State"}}
- {dropdownArrow && (
-
- )}
-
-
- );
-};
-
export const StateDropdown: React.FC = observer((props) => {
const {
button,
@@ -162,8 +41,8 @@ export const StateDropdown: React.FC = observer((props) => {
onChange,
placement,
projectId,
+ showTooltip = false,
tabIndex,
- tooltip = false,
value,
} = props;
// states
@@ -209,14 +88,35 @@ export const StateDropdown: React.FC = observer((props) => {
const selectedState = getStateById(value);
- const openDropdown = () => {
- setIsOpen(true);
+ const onOpen = () => {
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isOpen) setIsOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isOpen) onOpen();
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string) => {
+ onChange(val);
+ handleClose();
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
- onChange={onChange}
+ onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -235,7 +135,7 @@ export const StateDropdown: React.FC = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{button}
@@ -251,70 +151,30 @@ export const StateDropdown: React.FC = observer((props) => {
},
buttonContainerClassName
)}
- onClick={openDropdown}
+ onClick={handleOnClick}
>
- {buttonVariant === "border-with-text" ? (
-
- ) : buttonVariant === "border-without-text" ? (
-
- ) : buttonVariant === "background-with-text" ? (
-
- ) : buttonVariant === "background-without-text" ? (
-
- ) : buttonVariant === "transparent-with-text" ? (
-
- ) : buttonVariant === "transparent-without-text" ? (
-
- ) : null}
+
+ {!hideIcon && (
+
+ )}
+ {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
+ {selectedState?.name ?? "State"}
+ )}
+ {dropdownArrow && (
+
+ )}
+
)}
@@ -348,7 +208,6 @@ export const StateDropdown: React.FC = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
- onClick={closeDropdown}
>
{({ selected }) => (
<>
diff --git a/web/components/dropdowns/types.d.ts b/web/components/dropdowns/types.d.ts
index b99369022..128c7a525 100644
--- a/web/components/dropdowns/types.d.ts
+++ b/web/components/dropdowns/types.d.ts
@@ -17,7 +17,6 @@ export type TDropdownProps = {
hideIcon?: boolean;
placeholder?: string;
placement?: Placement;
+ showTooltip?: boolean;
tabIndex?: number;
- // TODO: rename this prop to showTooltip
- tooltip?: boolean;
};
diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx
index c15a33f8b..eff05aba5 100644
--- a/web/components/headers/project-settings.tsx
+++ b/web/components/headers/project-settings.tsx
@@ -2,13 +2,13 @@ import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// ui
-import { Breadcrumbs } from "@plane/ui";
+import { Breadcrumbs, CustomMenu } from "@plane/ui";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// hooks
import { useProject, useUser } from "hooks/store";
// constants
-import { EUserProjectRoles } from "constants/project";
+import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@@ -20,7 +20,7 @@ export const ProjectSettingHeader: FC = observer((props)
const { title } = props;
// router
const router = useRouter();
- const { workspaceSlug } = router.query;
+ const { workspaceSlug, projectId } = router.query;
// store hooks
const {
membership: { currentProjectRole },
@@ -31,29 +31,48 @@ export const ProjectSettingHeader: FC = observer((props)
return (
+
-
-
-
- {currentProjectDetails?.name.charAt(0)}
-
- )
- }
- link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
- />
-
-
+
+
+
+ {currentProjectDetails?.name.charAt(0)}
+
+ )
+ }
+ link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
+ />
+
+
+
+
+
+
{title}
+ }
+ placement="bottom-start"
+ closeOnSelect
+ >
+ {PROJECT_SETTINGS_LINKS.map((item) => (
+ router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}>
+ {item.label}
+
+ ))}
+
);
diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx
index 0086f17fe..625b7991c 100644
--- a/web/components/headers/workspace-settings.tsx
+++ b/web/components/headers/workspace-settings.tsx
@@ -1,12 +1,13 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui
-import { Breadcrumbs } from "@plane/ui";
+import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { Settings } from "lucide-react";
// hooks
import { observer } from "mobx-react-lite";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
+import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
export interface IWorkspaceSettingHeader {
title: string;
@@ -21,7 +22,7 @@ export const WorkspaceSettingHeader: FC = observer((pro
return (
-
+
= observer((pro
icon={}
link={`/${workspaceSlug}/settings`}
/>
-
+
+
+
+
{title}
+ }
+ placement="bottom-start"
+ closeOnSelect
+ >
+ {WORKSPACE_SETTINGS_LINKS.map((item) => (
+ router.push(`/${workspaceSlug}${item.href}`)}>
+ {item.label}
+
+ ))}
+
);
diff --git a/web/components/integration/jira/give-details.tsx b/web/components/integration/jira/give-details.tsx
index efeca487a..0d90ba0a5 100644
--- a/web/components/integration/jira/give-details.tsx
+++ b/web/components/integration/jira/give-details.tsx
@@ -7,6 +7,8 @@ import { Plus } from "lucide-react";
import { useApplication, useProject } from "hooks/store";
// components
import { CustomSelect, Input } from "@plane/ui";
+// helpers
+import { checkEmailValidity } from "helpers/string.helper";
// types
import { IJiraImporterForm } from "@plane/types";
@@ -46,17 +48,18 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => (
)}
/>
+ {errors.metadata?.api_token && {errors.metadata.api_token.message}
}
@@ -75,7 +78,6 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => (
{
/>
)}
/>
+ {errors.metadata?.project_key && (
+ {errors.metadata.project_key.message}
+ )}
@@ -100,11 +105,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.email"
rules={{
required: "Please enter email address.",
+ validate: (value) => checkEmailValidity(value) || "Please enter a valid email address",
}}
render={({ field: { value, onChange, ref } }) => (
{
/>
)}
/>
+ {errors.metadata?.email && {errors.metadata.email.message}
}
@@ -129,12 +135,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.cloud_hostname"
rules={{
required: "Please enter your cloud host name.",
+ validate: (value) => !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://",
}}
render={({ field: { value, onChange, ref } }) => (
{
/>
)}
/>
+ {errors.metadata?.cloud_hostname && (
+
{errors.metadata.cloud_hostname.message}
+ )}
diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx
index a5b0ef149..79b91ef40 100644
--- a/web/components/issues/draft-issue-form.tsx
+++ b/web/components/issues/draft-issue-form.tsx
@@ -21,7 +21,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
- ModuleSelectDropdown,
+ ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -257,14 +257,16 @@ export const DraftIssueForm: FC
= observer((props) => {
};
useEffect(() => {
- setFocus("name");
-
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
});
- }, [setFocus, prePopulatedData, reset, data]);
+ }, [prePopulatedData, reset, data]);
+
+ useEffect(() => {
+ setFocus("name");
+ }, [setFocus]);
// update projectId in form when projectId changes
useEffect(() => {
@@ -577,12 +579,12 @@ export const DraftIssueForm: FC = observer((props) => {
name="module_ids"
render={({ field: { value, onChange } }) => (
- onChange(moduleId)}
+ value={value ?? []}
+ onChange={onChange}
buttonVariant="border-with-text"
+ multiple
/>
)}
diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx
index 172a0bb82..bb79c9817 100644
--- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx
+++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx
@@ -10,7 +10,7 @@ import { TActivityOperations } from "../root";
import { TIssueComment } from "@plane/types";
// icons
import { Globe2, Lock } from "lucide-react";
-import { useWorkspace } from "hooks/store";
+import { useMention, useWorkspace } from "hooks/store";
const fileService = new FileService();
@@ -43,6 +43,8 @@ export const IssueCommentCreate: FC = (props) => {
const workspaceStore = useWorkspace();
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
+ const { mentionHighlights, mentionSuggestions } = useMention();
+
// refs
const editorRef = useRef(null);
// react hook form
@@ -61,7 +63,14 @@ export const IssueCommentCreate: FC = (props) => {
};
return (
-
+
{
+ // if (e.key === "Enter" && !e.shiftKey) {
+ // e.preventDefault();
+ // // handleSubmit(onSubmit)(e);
+ // }
+ // }}
+ >
= (props) => {
render={({ field: { value, onChange } }) => (
{
+ console.log("yo");
handleSubmit(onSubmit)(e);
}}
cancelUploadImage={fileService.cancelUpload}
@@ -86,6 +96,8 @@ export const IssueCommentCreate: FC = (props) => {
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
+ mentionSuggestions={mentionSuggestions}
+ mentionHighlights={mentionHighlights}
commentAccessSpecifier={
showAccessSpecifier
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx
index 82ff4ed32..1c4d80168 100644
--- a/web/components/issues/issue-detail/module-select.tsx
+++ b/web/components/issues/issue-detail/module-select.tsx
@@ -1,10 +1,9 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
-import xor from "lodash/xor";
// hooks
import { useIssueDetail } from "hooks/store";
// components
-import { ModuleSelectDropdown } from "components/dropdowns";
+import { ModuleDropdown } from "components/dropdowns";
// ui
import { Spinner } from "@plane/ui";
// helpers
@@ -33,56 +32,42 @@ export const IssueModuleSelect: React.FC = observer((props)
const issue = getIssueById(issueId);
const disableSelect = disabled || isUpdating;
- const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
- if (!issue) return;
+ const handleIssueModuleChange = async (moduleIds: string[]) => {
+ if (!issue || !issue.module_ids) return;
setIsUpdating(true);
- if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0)
- await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue?.module_ids);
- if (typeof moduleIds === "string" && moduleIds)
- await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]);
-
- if (Array.isArray(moduleIds)) {
- if (moduleIds.includes(undefined)) {
- await issueOperations.removeModulesFromIssue?.(
- workspaceSlug,
- projectId,
- issueId,
- moduleIds.filter((x) => x != undefined) as string[]
- );
- } else {
- const _moduleIds = xor(issue?.module_ids, moduleIds)[0];
- if (_moduleIds) {
- if (issue?.module_ids?.includes(_moduleIds))
- await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
- else await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
- }
- }
+ if (moduleIds.length === 0)
+ await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids);
+ else if (moduleIds.length > issue.module_ids.length) {
+ const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m));
+ await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds);
+ } else if (moduleIds.length < issue.module_ids.length) {
+ const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m));
+ await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds);
}
+
setIsUpdating(false);
};
return (
-
-
{isUpdating && }
);
diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx
index 7ad8c836f..9a1aa48ad 100644
--- a/web/components/issues/issue-detail/parent-select.tsx
+++ b/web/components/issues/issue-detail/parent-select.tsx
@@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC = observer((props)
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank"
rel="noopener noreferrer"
- className="text-xs font-medium mt-0.5"
+ className="text-xs font-medium"
onClick={(e) => e.stopPropagation()}
>
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx
index 1fdd353a6..67bba8697 100644
--- a/web/components/issues/issue-detail/relation-select.tsx
+++ b/web/components/issues/issue-detail/relation-select.tsx
@@ -132,7 +132,7 @@ export const IssueRelationSelect: React.FC = observer((pro
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank"
rel="noopener noreferrer"
- className="text-xs font-medium mt-0.5"
+ className="text-xs font-medium"
onClick={(e) => e.stopPropagation()}
>
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx
index 9a16dcbf0..fda73e94f 100644
--- a/web/components/issues/issue-detail/root.tsx
+++ b/web/components/issues/issue-detail/root.tsx
@@ -198,15 +198,15 @@ export const IssueDetailRoot: FC = (props) => {
try {
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
- title: "Module removed from issue successfully",
type: "success",
- message: "Module removed from issue successfully",
+ title: "Successful!",
+ message: "Issue removed from module successfully.",
});
} catch (error) {
setToastAlert({
- title: "Module remove from issue failed",
type: "error",
- message: "Module remove from issue failed",
+ title: "Error!",
+ message: "Issue could not be removed from module. Please try again.",
});
}
},
diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx
index f2ee876b9..668d3538f 100644
--- a/web/components/issues/issue-detail/sidebar.tsx
+++ b/web/components/issues/issue-detail/sidebar.tsx
@@ -233,7 +233,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
- showPlaceholderIcon
+ // TODO: add this logic
+ // showPlaceholderIcon
/>
@@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC
= observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
- showPlaceholderIcon
+ // TODO: add this logic
+ // showPlaceholderIcon
/>
diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx
index 3b3ef887e..7cb53ad39 100644
--- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx
+++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx
@@ -61,7 +61,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
projectId?.toString(),
issueStore,
issueMap,
- groupedIssueIds
+ groupedIssueIds,
+ viewId
).catch((err) => {
setToastAlert({
title: "Error",
diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx
index c23938a19..4057c7b93 100644
--- a/web/components/issues/issue-layouts/properties/all-properties.tsx
+++ b/web/components/issues/issue-layouts/properties/all-properties.tsx
@@ -81,7 +81,7 @@ export const IssueProperties: React.FC = observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
- tooltip
+ showTooltip
/>
@@ -95,7 +95,7 @@ export const IssueProperties: React.FC
= observer((props) => {
disabled={isReadOnly}
buttonVariant="border-without-text"
buttonClassName="border"
- tooltip
+ showTooltip
/>
@@ -123,7 +123,7 @@ export const IssueProperties: React.FC
= observer((props) => {
placeholder="Start date"
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
- tooltip
+ showTooltip
/>
@@ -139,7 +139,7 @@ export const IssueProperties: React.FC
= observer((props) => {
placeholder="Due date"
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
- tooltip
+ showTooltip
/>
@@ -169,7 +169,7 @@ export const IssueProperties: React.FC
= observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
- tooltip
+ showTooltip
/>
diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx
index f1f5d873f..703a9783f 100644
--- a/web/components/issues/issue-modal/form.tsx
+++ b/web/components/issues/issue-modal/form.tsx
@@ -20,7 +20,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
- ModuleSelectDropdown,
+ ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -267,6 +267,7 @@ export const IssueFormRoot: FC
= observer((props) => {
handleFormChange();
}}
buttonVariant="border-with-text"
+ // TODO: update tabIndex logic
tabIndex={19}
/>
@@ -547,18 +548,17 @@ export const IssueFormRoot: FC = observer((props) => {
name="module_ids"
render={({ field: { value, onChange } }) => (
- {
- onChange(moduleId);
+ value={value ?? []}
+ onChange={(moduleIds) => {
+ onChange(moduleIds);
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={13}
- multiple={true}
- showCount={true}
+ multiple
+ showCount
/>
)}
diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx
index ea00b845a..7a241e070 100644
--- a/web/components/issues/peek-overview/properties.tsx
+++ b/web/components/issues/peek-overview/properties.tsx
@@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC = observer((pro
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
- showPlaceholderIcon
+ // TODO: add this logic
+ // showPlaceholderIcon
/>
@@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC = observer((pro
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
- showPlaceholderIcon
+ // TODO: add this logic
+ // showPlaceholderIcon
/>
diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx
index 54931f85d..73fddfa9d 100644
--- a/web/components/issues/select/label.tsx
+++ b/web/components/issues/select/label.tsx
@@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC = observer((props) => {
const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
- const openDropdown = () => {
- setIsDropdownOpen(true);
+ const onOpen = () => {
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus();
};
- const closeDropdown = () => setIsDropdownOpen(false);
- const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen);
- useOutsideClickDetector(dropdownRef, closeDropdown);
+
+ const handleClose = () => {
+ if (isDropdownOpen) setIsDropdownOpen(false);
+ if (referenceElement) referenceElement.blur();
+ };
+
+ const toggleDropdown = () => {
+ if (!isDropdownOpen) onOpen();
+ setIsDropdownOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const dropdownOnChange = (val: string[]) => {
+ onChange(val);
+ };
+
+ const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
+
+ const handleOnClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleDropdown();
+ };
+
+ useOutsideClickDetector(dropdownRef, handleClose);
return (
= observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
value={value}
- onChange={(val) => onChange(val)}
+ onChange={dropdownOnChange}
className="relative flex-shrink-0 h-full"
multiple
disabled={disabled}
@@ -73,7 +93,7 @@ export const IssueLabelSelect: React.FC = observer((props) => {
type="button"
ref={setReferenceElement}
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
- onClick={openDropdown}
+ onClick={handleOnClick}
>
{label ? (
label
diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx
index 3badbb8bf..f4ab3e846 100644
--- a/web/components/project/form.tsx
+++ b/web/components/project/form.tsx
@@ -147,10 +147,10 @@ export const ProjectDetailsForm: FC = (props) => {
return (