From ee09f820b73a36e5906af529cd4b9fb6cfe57b1b Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 8 Jul 2025 18:54:49 +0530 Subject: [PATCH 1/4] [WEB-4457] refactor: decouple work item properties from mobx store --- apps/web/core/components/cycles/form.tsx | 2 +- apps/web/core/components/dropdowns/index.ts | 8 +- .../dropdowns/member/{index.tsx => base.tsx} | 50 +++--- .../components/dropdowns/member/dropdown.tsx | 48 +++++ .../dropdowns/member/member-options.tsx | 57 +++--- .../dropdowns/module/{index.tsx => base.tsx} | 166 +++--------------- .../dropdowns/module/button-content.tsx | 129 ++++++++++++++ .../components/dropdowns/module/dropdown.tsx | 56 ++++++ .../dropdowns/module/module-options.tsx | 35 ++-- .../{project.tsx => project/base.tsx} | 56 +++--- .../components/dropdowns/project/dropdown.tsx | 35 ++++ .../dropdowns/{state.tsx => state/base.tsx} | 101 +++++------ .../components/dropdowns/state/dropdown.tsx | 47 +++++ .../issue-modal/components/project-select.tsx | 2 +- .../components/issues/issue-modal/form.tsx | 14 +- .../issues/select/{label.tsx => base.tsx} | 92 +++++----- .../components/issues/select/dropdown.tsx | 35 ++++ .../core/components/issues/select/index.ts | 2 +- .../components/labels/create-label-modal.tsx | 16 +- apps/web/core/components/modules/form.tsx | 2 +- apps/web/helpers/react-hook-form.helper.ts | 27 +++ 21 files changed, 605 insertions(+), 375 deletions(-) rename apps/web/core/components/dropdowns/member/{index.tsx => base.tsx} (91%) create mode 100644 apps/web/core/components/dropdowns/member/dropdown.tsx rename apps/web/core/components/dropdowns/module/{index.tsx => base.tsx} (50%) create mode 100644 apps/web/core/components/dropdowns/module/button-content.tsx create mode 100644 apps/web/core/components/dropdowns/module/dropdown.tsx rename apps/web/core/components/dropdowns/{project.tsx => project/base.tsx} (90%) create mode 100644 apps/web/core/components/dropdowns/project/dropdown.tsx rename apps/web/core/components/dropdowns/{state.tsx => state/base.tsx} (88%) create mode 100644 apps/web/core/components/dropdowns/state/dropdown.tsx rename apps/web/core/components/issues/select/{label.tsx => base.tsx} (89%) create mode 100644 apps/web/core/components/issues/select/dropdown.tsx create mode 100644 apps/web/helpers/react-hook-form.helper.ts diff --git a/apps/web/core/components/cycles/form.tsx b/apps/web/core/components/cycles/form.tsx index bf98a38a647..6b0ec32f029 100644 --- a/apps/web/core/components/cycles/form.tsx +++ b/apps/web/core/components/cycles/form.tsx @@ -83,7 +83,7 @@ export const CycleForm: React.FC = (props) => { }} multiple={false} buttonVariant="border-with-text" - renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]} + renderCondition={(projectId) => !!projectsWithCreatePermissions?.[projectId]} tabIndex={getIndex("cover_image")} /> diff --git a/apps/web/core/components/dropdowns/index.ts b/apps/web/core/components/dropdowns/index.ts index 0948f5e75e8..3455fa7f474 100644 --- a/apps/web/core/components/dropdowns/index.ts +++ b/apps/web/core/components/dropdowns/index.ts @@ -1,10 +1,10 @@ -export * from "./member"; +export * from "./member/dropdown"; export * from "./cycle"; export * from "./date-range"; export * from "./date"; export * from "./estimate"; export * from "./merged-date"; -export * from "./module"; +export * from "./module/dropdown"; export * from "./priority"; -export * from "./project"; -export * from "./state"; +export * from "./project/dropdown"; +export * from "./state/dropdown"; diff --git a/apps/web/core/components/dropdowns/member/index.tsx b/apps/web/core/components/dropdowns/member/base.tsx similarity index 91% rename from apps/web/core/components/dropdowns/member/index.tsx rename to apps/web/core/components/dropdowns/member/base.tsx index acfa9f46092..ef17e94828c 100644 --- a/apps/web/core/components/dropdowns/member/index.tsx +++ b/apps/web/core/components/dropdowns/member/base.tsx @@ -1,33 +1,32 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react"; import { ChevronDown, LucideIcon } from "lucide-react"; +// plane imports import { useTranslation } from "@plane/i18n"; -// ui +import { IUserLite } from "@plane/types"; import { ComboDropDown } from "@plane/ui"; // helpers import { cn } from "@plane/utils"; // hooks -import { useMember } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; -// components +// local imports import { DropdownButton } from "../buttons"; import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { ButtonAvatars } from "./avatar"; -// constants import { MemberOptions } from "./member-options"; -// types import { MemberDropdownProps } from "./types"; -type Props = { - projectId?: string; +type TMemberDropdownBaseProps = { + getUserDetails: (userId: string) => IUserLite | undefined; icon?: LucideIcon; + memberIds?: string[]; onClose?: () => void; - renderByDefault?: boolean; + onDropdownOpen?: () => void; optionsClassName?: string; - memberIds?: string[]; + renderByDefault?: boolean; } & MemberDropdownProps; -export const MemberDropdown: React.FC = observer((props) => { +export const MemberDropdownBase: React.FC = observer((props) => { const { t } = useTranslation(); const { button, @@ -38,39 +37,37 @@ export const MemberDropdown: React.FC = observer((props) => { disabled = false, dropdownArrow = false, dropdownArrowClassName = "", - optionsClassName = "", + getUserDetails, hideIcon = false, + icon, + memberIds, multiple, onChange, onClose, + onDropdownOpen, + optionsClassName = "", placeholder = t("members"), - tooltipContent, placement, - projectId, + renderByDefault = true, showTooltip = false, showUserDetails = false, tabIndex, + tooltipContent, value, - icon, - renderByDefault = true, - memberIds, } = props; - // states - const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); + // states + const [isOpen, setIsOpen] = useState(false); - const { getUserDetails } = useMember(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const comboboxProps: any = { + const comboboxProps = { value, onChange, disabled, + multiple, }; - if (multiple) comboboxProps.multiple = true; const { handleClose, handleKeyDown, handleOnClick } = useDropdown({ dropdownRef, @@ -163,19 +160,20 @@ export const MemberDropdown: React.FC = observer((props) => { {isOpen && ( diff --git a/apps/web/core/components/dropdowns/member/dropdown.tsx b/apps/web/core/components/dropdowns/member/dropdown.tsx new file mode 100644 index 00000000000..e9f42a50e11 --- /dev/null +++ b/apps/web/core/components/dropdowns/member/dropdown.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { LucideIcon } from "lucide-react"; +// hooks +import { useMember } from "@/hooks/store"; +// local imports +import { MemberDropdownBase } from "./base"; +import { MemberDropdownProps } from "./types"; + +type TMemberDropdownProps = { + icon?: LucideIcon; + memberIds?: string[]; + onClose?: () => void; + optionsClassName?: string; + projectId?: string; + renderByDefault?: boolean; +} & MemberDropdownProps; + +export const MemberDropdown: React.FC = observer((props) => { + const { memberIds: propsMemberIds, projectId } = props; + // router params + const { workspaceSlug } = useParams(); + // store hooks + const { + getUserDetails, + project: { getProjectMemberIds, fetchProjectMembers }, + workspace: { workspaceMemberIds }, + } = useMember(); + + const memberIds = propsMemberIds + ? propsMemberIds + : projectId + ? getProjectMemberIds(projectId, false) + : workspaceMemberIds; + + const onDropdownOpen = () => { + if (!memberIds && projectId && workspaceSlug) fetchProjectMembers(workspaceSlug.toString(), projectId); + }; + + return ( + + ); +}); diff --git a/apps/web/core/components/dropdowns/member/member-options.tsx b/apps/web/core/components/dropdowns/member/member-options.tsx index f2cea10faba..e7bee051e02 100644 --- a/apps/web/core/components/dropdowns/member/member-options.tsx +++ b/apps/web/core/components/dropdowns/member/member-options.tsx @@ -3,46 +3,48 @@ import { useEffect, useRef, useState } from "react"; import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; import { Check, Search } from "lucide-react"; import { Combobox } from "@headlessui/react"; -import { EUserPermissions } from "@plane/constants"; +// plane imports import { useTranslation } from "@plane/i18n"; -// plane ui import { Avatar } from "@plane/ui"; import { cn, getFileURL } from "@plane/utils"; -// helpers // hooks -import { useUser, useMember } from "@/hooks/store"; +import { useUser } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { IUserLite } from "@plane/types"; interface Props { - memberIds?: string[]; className?: string; + getUserDetails: (userId: string) => IUserLite | undefined; + isOpen: boolean; + memberIds?: string[]; + onDropdownOpen?: () => void; optionsClassName?: string; - projectId?: string; - referenceElement: HTMLButtonElement | null; placement: Placement | undefined; - isOpen: boolean; + referenceElement: HTMLButtonElement | null; } export const MemberOptions: React.FC = observer((props: Props) => { - const { memberIds: propsMemberIds, projectId, referenceElement, placement, isOpen, optionsClassName = "" } = props; + const { + getUserDetails, + isOpen, + memberIds, + onDropdownOpen, + optionsClassName = "", + placement, + referenceElement, + } = props; + // refs + const inputRef = useRef(null); // states const [query, setQuery] = useState(""); const [popperElement, setPopperElement] = useState(null); - // refs - const inputRef = useRef(null); - // store hooks + // plane hooks const { t } = useTranslation(); - const { workspaceSlug } = useParams(); - const { - getUserDetails, - project: { getProjectMemberIds, fetchProjectMembers, getProjectMemberDetails }, - workspace: { workspaceMemberIds }, - } = useMember(); + // store hooks const { data: currentUser } = useUser(); const { isMobile } = usePlatformOS(); // popper-js init @@ -60,22 +62,13 @@ export const MemberOptions: React.FC = observer((props: Props) => { useEffect(() => { if (isOpen) { - onOpen(); + onDropdownOpen?.(); if (!isMobile) { inputRef.current && inputRef.current.focus(); } } }, [isOpen, isMobile]); - const memberIds = propsMemberIds - ? propsMemberIds - : projectId - ? getProjectMemberIds(projectId, true) - : workspaceMemberIds; - const onOpen = () => { - if (!memberIds && workspaceSlug && projectId) fetchProjectMembers(workspaceSlug.toString(), projectId); - }; - const searchInputKeyDown = (e: React.KeyboardEvent) => { if (query !== "" && e.key === "Escape") { e.stopPropagation(); @@ -86,12 +79,6 @@ export const MemberOptions: React.FC = observer((props: Props) => { const options = memberIds ?.map((userId) => { const userDetails = getUserDetails(userId); - if (projectId) { - const role = getProjectMemberDetails(userId, projectId)?.role; - const isGuest = role === EUserPermissions.GUEST; - if (isGuest) return; - } - return { value: userId, query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, diff --git a/apps/web/core/components/dropdowns/module/index.tsx b/apps/web/core/components/dropdowns/module/base.tsx similarity index 50% rename from apps/web/core/components/dropdowns/module/index.tsx rename to apps/web/core/components/dropdowns/module/base.tsx index 9e789dbf4e7..5e0965f9a74 100644 --- a/apps/web/core/components/dropdowns/module/index.tsx +++ b/apps/web/core/components/dropdowns/module/base.tsx @@ -2,34 +2,33 @@ import { ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; -import { ChevronDown, X } from "lucide-react"; -// i18n +// plane imports import { useTranslation } from "@plane/i18n"; -// ui -import { ComboDropDown, DiceIcon, Tooltip } from "@plane/ui"; -// helpers +import { IModule } from "@plane/types"; +import { ComboDropDown } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks -import { useModule } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// components +// local imports import { DropdownButton } from "../buttons"; import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; -// types import { TDropdownProps } from "../types"; -// constants +import { ModuleButtonContent } from "./button-content"; import { ModuleOptions } from "./module-options"; -type Props = TDropdownProps & { +type TModuleDropdownBaseProps = TDropdownProps & { button?: ReactNode; dropdownArrow?: boolean; dropdownArrowClassName?: string; - projectId: string | undefined; - showCount?: boolean; + getModuleById: (moduleId: string) => IModule | null; + itemClassName?: string; + moduleIds?: string[]; onClose?: () => void; + onDropdownOpen?: () => void; + projectId: string | undefined; renderByDefault?: boolean; - itemClassName?: string; + showCount?: boolean; } & ( | { multiple: false; @@ -43,149 +42,31 @@ type Props = TDropdownProps & { } ); -type ButtonContentProps = { - disabled: boolean; - dropdownArrow: boolean; - dropdownArrowClassName: string; - hideIcon: boolean; - hideText: boolean; - onChange: (moduleIds: string[]) => void; - placeholder?: string; - showCount: boolean; - showTooltip?: boolean; - value: string | string[] | null; - className?: string; -}; - -const ButtonContent: React.FC = (props) => { - const { - disabled, - dropdownArrow, - dropdownArrowClassName, - hideIcon, - hideText, - onChange, - placeholder, - showCount, - showTooltip = false, - value, - className, - } = props; - // store hooks - const { getModuleById } = useModule(); - const { isMobile } = usePlatformOS(); - - if (Array.isArray(value)) - return ( - <> - {showCount ? ( -
- {!hideIcon && } - {(value.length > 0 || !!placeholder) && ( -
- {value.length > 0 - ? value.length === 1 - ? `${getModuleById(value[0])?.name || "module"}` - : `${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} - - )} - {dropdownArrow && ( -
diff --git a/apps/web/core/components/dropdowns/module/button-content.tsx b/apps/web/core/components/dropdowns/module/button-content.tsx new file mode 100644 index 00000000000..3fa08ca1953 --- /dev/null +++ b/apps/web/core/components/dropdowns/module/button-content.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { ChevronDown, X } from "lucide-react"; +// plane imports +import { DiceIcon, Tooltip } from "@plane/ui"; +import { cn } from "@plane/utils"; +// hooks +import { useModule } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +type ModuleButtonContentProps = { + disabled: boolean; + dropdownArrow: boolean; + dropdownArrowClassName: string; + hideIcon: boolean; + hideText: boolean; + onChange: (moduleIds: string[]) => void; + placeholder?: string; + showCount: boolean; + showTooltip?: boolean; + value: string | string[] | null; + className?: string; +}; + +export const ModuleButtonContent: React.FC = (props) => { + const { + disabled, + dropdownArrow, + dropdownArrowClassName, + hideIcon, + hideText, + onChange, + placeholder, + showCount, + showTooltip = false, + value, + className, + } = props; + // store hooks + const { getModuleById } = useModule(); + const { isMobile } = usePlatformOS(); + + if (Array.isArray(value)) + return ( + <> + {showCount ? ( +
+ {!hideIcon && } + {(value.length > 0 || !!placeholder) && ( +
+ {value.length > 0 + ? value.length === 1 + ? `${getModuleById(value[0])?.name || "module"}` + : `${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} + + )} + {dropdownArrow && ( +