[WEB-4457] refactor: decouple work item properties from mobx store (#7363)

This commit is contained in:
Prateek Shourya 2025-07-18 20:38:21 +05:30 committed by GitHub
parent 5660b28574
commit f3daac6f95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 649 additions and 428 deletions

View File

@ -1,9 +0,0 @@
type TIssueAdditionalPropertiesProps = {
issueId: string | undefined;
issueTypeId: string | null;
projectId: string;
workspaceSlug: string;
isDraft?: boolean;
};
export const IssueAdditionalProperties: React.FC<TIssueAdditionalPropertiesProps> = () => <></>;

View File

@ -1,4 +1,3 @@
export * from "./provider";
export * from "./issue-type-select";
export * from "./additional-properties";
export * from "./template-select";

View File

@ -0,0 +1,10 @@
import React from "react";
export type TWorkItemModalAdditionalPropertiesProps = {
isDraft?: boolean;
projectId: string | null;
workItemId: string | undefined;
workspaceSlug: string;
};
export const WorkItemModalAdditionalProperties: React.FC<TWorkItemModalAdditionalPropertiesProps> = () => null;

View File

@ -44,6 +44,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
handleProjectEntitiesFetch: () => Promise.resolve(),
handleTemplateChange: () => Promise.resolve(),
handleConvert: () => Promise.resolve(),
handleCreateSubWorkItem: () => Promise.resolve(),
}}
>
{children}

View File

@ -83,7 +83,7 @@ export const CycleForm: React.FC<Props> = (props) => {
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]}
renderCondition={(projectId) => !!projectsWithCreatePermissions?.[projectId]}
tabIndex={getIndex("cover_image")}
/>
</div>

View File

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

View File

@ -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;
onClose?: () => void;
renderByDefault?: boolean;
optionsClassName?: string;
memberIds?: string[];
onClose?: () => void;
onDropdownOpen?: () => void;
optionsClassName?: string;
renderByDefault?: boolean;
} & MemberDropdownProps;
export const MemberDropdown: React.FC<Props> = observer((props) => {
export const MemberDropdownBase: React.FC<TMemberDropdownBaseProps> = observer((props) => {
const { t } = useTranslation();
const {
button,
@ -38,39 +37,37 @@ export const MemberDropdown: React.FC<Props> = 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<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(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<Props> = observer((props) => {
<ComboDropDown
as="div"
ref={dropdownRef}
{...comboboxProps}
className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
{...comboboxProps}
>
{isOpen && (
<MemberOptions
memberIds={memberIds}
optionsClassName={optionsClassName}
getUserDetails={getUserDetails}
isOpen={isOpen}
projectId={projectId}
memberIds={memberIds}
onDropdownOpen={onDropdownOpen}
optionsClassName={optionsClassName}
placement={placement}
referenceElement={referenceElement}
/>

View File

@ -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<TMemberDropdownProps> = 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 (
<MemberDropdownBase
{...props}
getUserDetails={getUserDetails}
memberIds={memberIds ?? []}
onDropdownOpen={onDropdownOpen}
/>
);
});

View File

@ -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;
optionsClassName?: string;
projectId?: string;
referenceElement: HTMLButtonElement | null;
placement: Placement | undefined;
getUserDetails: (userId: string) => IUserLite | undefined;
isOpen: boolean;
memberIds?: string[];
onDropdownOpen?: () => void;
optionsClassName?: string;
placement: Placement | undefined;
referenceElement: HTMLButtonElement | null;
}
export const MemberOptions: React.FC<Props> = 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<HTMLInputElement | null>(null);
// states
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// refs
const inputRef = useRef<HTMLInputElement | null>(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<Props> = 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<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
@ -86,12 +79,6 @@ export const MemberOptions: React.FC<Props> = 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}`,

View File

@ -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;
onClose?: () => void;
renderByDefault?: boolean;
getModuleById: (moduleId: string) => IModule | null;
itemClassName?: string;
moduleIds?: string[];
onClose?: () => void;
onDropdownOpen?: () => void;
projectId: string | undefined;
renderByDefault?: boolean;
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<ButtonContentProps> = (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 ? (
<div className="relative flex items-center max-w-full gap-1">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{(value.length > 0 || !!placeholder) && (
<div className="max-w-40 flex-grow truncate">
{value.length > 0
? value.length === 1
? `${getModuleById(value[0])?.name || "module"}`
: `${value.length} Module${value.length === 1 ? "" : "s"}`
: placeholder}
</div>
)}
</div>
) : value.length > 0 ? (
<div className="flex max-w-full flex-grow flex-wrap items-center gap-2 truncate py-0.5 ">
{value.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return (
<div
key={moduleId}
className={cn(
"flex max-w-full items-center gap-1 rounded bg-custom-background-80 py-1 text-custom-text-200",
className
)}
>
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
{!hideText && (
<Tooltip
tooltipHeading="Title"
tooltipContent={moduleDetails?.name}
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={false}
>
<span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span>
</Tooltip>
)}
{!disabled && (
<Tooltip
tooltipContent="Remove"
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={false}
>
<button
type="button"
className="flex-shrink-0"
onClick={() => {
const newModuleIds = value.filter((m) => m !== moduleId);
onChange(newModuleIds);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</button>
</Tooltip>
)}
</div>
);
})}
</div>
) : (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
<span className="flex-grow truncate text-left">{placeholder}</span>
</>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</>
);
else
return (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="flex-grow truncate text-left">{value ? getModuleById(value)?.name : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</>
);
};
export const ModuleDropdown: React.FC<Props> = observer((props) => {
export const ModuleDropdownBase: React.FC<TModuleDropdownBaseProps> = observer((props) => {
const {
button,
buttonClassName,
itemClassName = "",
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
getModuleById,
hideIcon = false,
itemClassName = "",
moduleIds,
multiple,
onChange,
onClose,
placeholder = "",
placement,
projectId,
renderByDefault = true,
showCount = false,
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// i18n
const { t } = useTranslation();
@ -199,8 +80,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
// store hooks
const { isMobile } = usePlatformOS();
const { getModuleNameById } = useModule();
const { handleClose, handleKeyDown, handleOnClick } = useDropdown({
dropdownRef,
inputRef,
@ -214,12 +93,12 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
if (!multiple) handleClose();
};
const comboboxProps: any = {
const comboboxProps = {
value,
onChange: dropdownOnChange,
disabled,
multiple,
};
if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (isOpen && inputRef.current && !isMobile) {
@ -266,7 +145,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
tooltipContent={
Array.isArray(value)
? `${value
.map((moduleId) => getModuleNameById(moduleId))
.map((moduleId) => getModuleById(moduleId)?.name)
.toString()
.replaceAll(",", ", ")}`
: ""
@ -275,7 +154,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
variant={buttonVariant}
renderToolTipByDefault={renderByDefault}
>
<ButtonContent
<ModuleButtonContent
disabled={disabled}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
@ -307,10 +186,11 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
{isOpen && projectId && (
<ModuleOptions
isOpen={isOpen}
projectId={projectId}
placement={placement}
referenceElement={referenceElement}
multiple={multiple}
getModuleById={getModuleById}
moduleIds={moduleIds}
/>
)}
</ComboDropDown>

View File

@ -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<ModuleButtonContentProps> = (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 ? (
<div className="relative flex items-center max-w-full gap-1">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{(value.length > 0 || !!placeholder) && (
<div className="max-w-40 flex-grow truncate">
{value.length > 0
? value.length === 1
? `${getModuleById(value[0])?.name || "module"}`
: `${value.length} Module${value.length === 1 ? "" : "s"}`
: placeholder}
</div>
)}
</div>
) : value.length > 0 ? (
<div className="flex max-w-full flex-grow flex-wrap items-center gap-2 truncate py-0.5 ">
{value.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return (
<div
key={moduleId}
className={cn(
"flex max-w-full items-center gap-1 rounded bg-custom-background-80 py-1 text-custom-text-200",
className
)}
>
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
{!hideText && (
<Tooltip
tooltipHeading="Title"
tooltipContent={moduleDetails?.name}
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={false}
>
<span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span>
</Tooltip>
)}
{!disabled && (
<Tooltip
tooltipContent="Remove"
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={false}
>
<button
type="button"
className="flex-shrink-0"
onClick={() => {
const newModuleIds = value.filter((m) => m !== moduleId);
onChange(newModuleIds);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</button>
</Tooltip>
)}
</div>
);
})}
</div>
) : (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
<span className="flex-grow truncate text-left">{placeholder}</span>
</>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</>
);
else
return (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="flex-grow truncate text-left">{value ? getModuleById(value)?.name : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</>
);
};

View File

@ -0,0 +1,56 @@
"use client";
import { ReactNode } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks
import { useModule } from "@/hooks/store";
// types
import { TDropdownProps } from "../types";
// local imports
import { ModuleDropdownBase } from "./base";
type TModuleDropdownProps = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
projectId: string | undefined;
showCount?: boolean;
onClose?: () => void;
renderByDefault?: boolean;
itemClassName?: string;
} & (
| {
multiple: false;
onChange: (val: string | null) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[] | null;
}
);
export const ModuleDropdown: React.FC<TModuleDropdownProps> = observer((props) => {
const { projectId } = props;
// router
const { workspaceSlug } = useParams();
// store hooks
const { getModuleById, getProjectModuleIds, fetchModules } = useModule();
// derived values
const moduleIds = projectId ? getProjectModuleIds(projectId) : [];
const onDropdownOpen = () => {
if (!moduleIds && projectId && workspaceSlug) fetchModules(workspaceSlug.toString(), projectId);
};
return (
<ModuleDropdownBase
{...props}
getModuleById={getModuleById}
moduleIds={moduleIds ?? []}
onDropdownOpen={onDropdownOpen}
/>
);
});

View File

@ -3,21 +3,16 @@
import { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// i18n
// plane imports
import { useTranslation } from "@plane/i18n";
//components
import { IModule } from "@plane/types";
import { DiceIcon } from "@plane/ui";
//store
import { cn } from "@plane/utils";
import { useModule } from "@/hooks/store";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
//hooks
//icon
//types
type DropdownOptions =
| {
@ -28,25 +23,25 @@ type DropdownOptions =
| undefined;
interface Props {
projectId: string;
referenceElement: HTMLButtonElement | null;
placement: Placement | undefined;
getModuleById: (moduleId: string) => IModule | null;
isOpen: boolean;
moduleIds?: string[];
multiple: boolean;
onDropdownOpen?: () => void;
placement: Placement | undefined;
referenceElement: HTMLButtonElement | null;
}
export const ModuleOptions = observer((props: Props) => {
const { projectId, isOpen, referenceElement, placement, multiple } = props;
// i18n
const { t } = useTranslation();
const { getModuleById, isOpen, moduleIds, multiple, onDropdownOpen, placement, referenceElement } = props;
// refs
const inputRef = useRef<HTMLInputElement | null>(null);
// states
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// refs
const inputRef = useRef<HTMLInputElement | null>(null);
// plane hooks
const { t } = useTranslation();
// store hooks
const { workspaceSlug } = useParams();
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
const { isMobile } = usePlatformOS();
useEffect(() => {
@ -72,10 +67,8 @@ export const ModuleOptions = observer((props: Props) => {
],
});
const moduleIds = getProjectModuleIds(projectId);
const onOpen = () => {
if (workspaceSlug && !moduleIds) fetchModules(workspaceSlug.toString(), projectId);
onDropdownOpen?.();
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

View File

@ -3,33 +3,31 @@ import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Briefcase, Check, ChevronDown, Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
// plane imports
import { useTranslation } from "@plane/i18n";
import { ComboDropDown } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { Logo } from "@/components/common";
// helpers
// hooks
import { useProject } from "@/hooks/store";
import { useDropdown } from "@/hooks/use-dropdown";
// plane web types
import { TProject } from "@/plane-web/types";
// components
import { DropdownButton } from "./buttons";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
// types
import { TDropdownProps } from "./types";
import { Logo } from "@/components/common";
// hooks
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { TProject } from "@/plane-web/types";
// local imports
import { DropdownButton } from "../buttons";
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
import { TDropdownProps } from "../types";
type Props = TDropdownProps & {
button?: ReactNode;
currentProjectId?: string;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
getProjectById: (projectId: string | null | undefined) => Partial<TProject> | undefined;
onClose?: () => void;
renderCondition?: (project: TProject) => boolean;
projectIds: string[];
renderByDefault?: boolean;
currentProjectId?: string;
renderCondition?: (projectId: string) => boolean;
} & (
| {
multiple: false;
@ -43,38 +41,42 @@ type Props = TDropdownProps & {
}
);
export const ProjectDropdown: React.FC<Props> = observer((props) => {
export const ProjectDropdownBase: React.FC<Props> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
currentProjectId,
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
getProjectById,
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Project",
placement,
projectIds,
renderByDefault = true,
renderCondition,
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
currentProjectId,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// plane hooks
const { t } = useTranslation();
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
@ -88,17 +90,15 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
],
});
// store hooks
const { joinedProjectIds, getProjectById } = useProject();
const { t } = useTranslation();
const options = joinedProjectIds?.map((projectId) => {
const options = projectIds?.map((projectId) => {
const projectDetails = getProjectById(projectId);
if (renderCondition && projectDetails && !renderCondition(projectDetails)) return;
if (renderCondition && !renderCondition(projectId)) return;
return {
value: projectId,
query: `${projectDetails?.name}`,
content: (
<div className="flex items-center gap-2">
{projectDetails && (
{projectDetails?.logo_props && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails?.logo_props} size={12} />
</span>
@ -139,9 +139,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
};
const getProjectIcon = (value: string | string[] | null) => {
const renderIcon = (projectDetails: TProject) => (
const renderIcon = (logoProps: TProject["logo_props"]) => (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails.logo_props} size={14} />
<Logo logo={logoProps} size={14} />
</span>
);
@ -151,7 +151,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
{value.length > 0 ? (
value.map((projectId) => {
const projectDetails = getProjectById(projectId);
return projectDetails ? renderIcon(projectDetails) : null;
return projectDetails?.logo_props ? renderIcon(projectDetails.logo_props) : null;
})
) : (
<Briefcase className="size-3 text-custom-text-300" />
@ -160,7 +160,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
);
} else {
const projectDetails = getProjectById(value);
return projectDetails ? renderIcon(projectDetails) : null;
return projectDetails?.logo_props ? renderIcon(projectDetails.logo_props) : null;
}
};

View File

@ -0,0 +1,35 @@
import { ReactNode } from "react";
import { observer } from "mobx-react";
// hooks
import { useProject } from "@/hooks/store";
// local imports
import { TDropdownProps } from "../types";
import { ProjectDropdownBase } from "./base";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onClose?: () => void;
renderCondition?: (projectId: string) => boolean;
renderByDefault?: boolean;
currentProjectId?: string;
} & (
| {
multiple: false;
onChange: (val: string) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[];
}
);
export const ProjectDropdown: React.FC<Props> = observer((props) => {
// store hooks
const { joinedProjectIds, getProjectById } = useProject();
return <ProjectDropdownBase {...props} getProjectById={getProjectById} projectIds={joinedProjectIds} />;
});

View File

@ -2,45 +2,44 @@
import { ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { ChevronDown, Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { IState } from "@plane/types";
import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
// hooks
import { useProjectState } from "@/hooks/store";
import { useDropdown } from "@/hooks/use-dropdown";
// Plane-web
import { StateOption } from "@/plane-web/components/workflow";
// components
import { DropdownButton } from "./buttons";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
// types
import { TDropdownProps } from "./types";
import { DropdownButton } from "@/components/dropdowns/buttons";
import { BUTTON_VARIANTS_WITH_TEXT } from "@/components/dropdowns/constants";
import { TDropdownProps } from "@/components/dropdowns/types";
// hooks
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { StateOption } from "@/plane-web/components/workflow";
type Props = TDropdownProps & {
export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
alwaysAllowStateChange?: boolean;
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
filterAvailableStateIds?: boolean;
getStateById: (stateId: string | null | undefined) => IState | undefined;
iconSize?: string;
isForWorkItemCreation?: boolean;
isInitializing?: boolean;
onChange: (val: string) => void;
onClose?: () => void;
onDropdownOpen?: () => void;
projectId: string | undefined;
showDefaultState?: boolean;
value: string | undefined | null;
renderByDefault?: boolean;
stateIds?: string[];
filterAvailableStateIds?: boolean;
isForWorkItemCreation?: boolean;
alwaysAllowStateChange?: boolean;
iconSize?: string;
showDefaultState?: boolean;
stateIds: string[];
value: string | undefined | null;
};
export const StateDropdown: React.FC<Props> = observer((props) => {
export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps> = observer((props) => {
const {
button,
buttonClassName,
@ -50,29 +49,35 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
getStateById,
hideIcon = false,
iconSize = "size-4",
isInitializing = false,
onChange,
onClose,
onDropdownOpen,
placement,
projectId,
renderByDefault = true,
showDefaultState = true,
showTooltip = false,
stateIds,
tabIndex,
value,
renderByDefault = true,
stateIds,
iconSize = "size-4",
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [stateLoader, setStateLoader] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// store hooks
const { t } = useTranslation();
const statesList = stateIds.map((stateId) => getStateById(stateId)).filter((state) => !!state);
const defaultState = statesList?.find((state) => state?.default);
const stateValue = !!value ? value : showDefaultState ? defaultState?.id : undefined;
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
@ -85,16 +90,19 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
},
],
});
// store hooks
const { t } = useTranslation();
const { workspaceSlug } = useParams();
const { fetchProjectStates, getProjectStates, getStateById } = useProjectState();
const statesList = stateIds
? stateIds.map((stateId) => getStateById(stateId)).filter((state) => !!state)
: getProjectStates(projectId);
const defaultState = statesList?.find((state) => state?.default);
const stateValue = !!value ? value : showDefaultState ? defaultState?.id : undefined;
// dropdown init
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
isOpen,
onClose,
onOpen: onDropdownOpen,
query,
setIsOpen,
setQuery,
});
// derived values
const options = statesList?.map((state) => ({
value: state?.id,
query: `${state?.name}`,
@ -116,25 +124,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const selectedState = stateValue ? getStateById(stateValue) : undefined;
const onOpen = async () => {
if (!statesList && workspaceSlug && projectId) {
setStateLoader(true);
await fetchProjectStates(workspaceSlug.toString(), projectId);
setStateLoader(false);
}
};
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
isOpen,
onClose,
onOpen,
query,
setIsOpen,
setQuery,
});
const dropdownOnChange = (val: string) => {
onChange(val);
handleClose();
@ -178,7 +167,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
variant={buttonVariant}
renderToolTipByDefault={renderByDefault}
>
{stateLoader ? (
{isInitializing ? (
<Spinner className="h-3.5 w-3.5" />
) : (
<>

View File

@ -0,0 +1,47 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks
import { useProjectState } from "@/hooks/store";
// local imports
import { WorkItemStateDropdownBase, TWorkItemStateDropdownBaseProps } from "./base";
type TWorkItemStateDropdownProps = Omit<
TWorkItemStateDropdownBaseProps,
"stateIds" | "getStateById" | "onDropdownOpen" | "isInitializing"
> & {
stateIds?: string[];
};
export const StateDropdown: React.FC<TWorkItemStateDropdownProps> = observer((props) => {
const { projectId, stateIds: propsStateIds } = props;
// router params
const { workspaceSlug } = useParams();
// states
const [stateLoader, setStateLoader] = useState(false);
// store hooks
const { fetchProjectStates, getProjectStateIds, getStateById } = useProjectState();
// derived values
const stateIds = propsStateIds ?? getProjectStateIds(projectId);
// fetch states if not provided
const onDropdownOpen = async () => {
if (stateIds === undefined && workspaceSlug && projectId) {
setStateLoader(true);
await fetchProjectStates(workspaceSlug.toString(), projectId);
setStateLoader(false);
}
};
return (
<WorkItemStateDropdownBase
{...props}
getStateById={getStateById}
isInitializing={stateLoader}
stateIds={stateIds ?? []}
onDropdownOpen={onDropdownOpen}
/>
);
});

View File

@ -68,7 +68,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT);
const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
const { fetchIssue } = useIssueDetail();
const { allowedProjectIds, handleCreateUpdatePropertyValues } = useIssueModal();
const { allowedProjectIds, handleCreateUpdatePropertyValues, handleCreateSubWorkItem } = useIssueModal();
const { getProjectByIdentifier } = useProject();
// current store details
const { createIssue, updateIssue } = useIssuesActions(storeType);
@ -222,6 +222,13 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
workspaceSlug: workspaceSlug?.toString(),
isDraft: is_draft_issue,
});
// create sub work item
await handleCreateSubWorkItem({
workspaceSlug: workspaceSlug?.toString(),
projectId: response.project_id,
parentId: response.id,
});
}
setToast({

View File

@ -46,7 +46,7 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => allowedProjectIds.includes(project.id)}
renderCondition={(projectId) => allowedProjectIds.includes(projectId)}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>

View File

@ -1,12 +1,12 @@
import { createContext } from "react";
// ce imports
// react-hook-form
import { TIssueFields } from "ce/components/issues";
import { UseFormReset, UseFormWatch } from "react-hook-form";
// plane imports
import { EditorRefApi } from "@plane/editor";
import { ISearchIssueResponse, TIssue } from "@plane/types";
import { TIssuePropertyValues, TIssuePropertyValueErrors } from "@/plane-web/types/issue-types";
import { TIssueFields } from "ce/components/issues";
export type TPropertyValuesValidationProps = {
projectId: string | null;
@ -28,6 +28,12 @@ export type TCreateUpdatePropertyValuesProps = {
isDraft?: boolean;
};
export type TCreateSubWorkItemProps = {
workspaceSlug: string;
projectId: string;
parentId: string;
};
export type THandleTemplateChangeProps = {
workspaceSlug: string;
reset: UseFormReset<TIssue>;
@ -35,8 +41,9 @@ export type THandleTemplateChangeProps = {
};
export type THandleProjectEntitiesFetchProps = {
workItemProjectId: string | null | undefined;
workItemTypeId: string | undefined;
workspaceSlug: string;
templateId: string;
};
export type THandleParentWorkItemDetailsProps = {
@ -65,6 +72,7 @@ export type TIssueModalContext = {
handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise<void>;
handleTemplateChange: (props: THandleTemplateChangeProps) => Promise<void>;
handleConvert: (workspaceSlug: string, data: Partial<TIssue>) => Promise<void>;
handleCreateSubWorkItem: (props: TCreateSubWorkItemProps) => Promise<void>;
};
export const IssueModalContext = createContext<TIssueModalContext | undefined>(undefined);

View File

@ -32,16 +32,13 @@ import { CreateLabelModal } from "@/components/labels";
// helpers
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues } from "@/hooks/store";
import { useIssueDetail, useLabel, useProject, useProjectState, useWorkspaceDraftIssues } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
// plane web imports
import { DeDupeButtonRoot, DuplicateModalRoot } from "@/plane-web/components/de-dupe";
import {
IssueAdditionalProperties,
IssueTypeSelect,
WorkItemTemplateSelect,
} from "@/plane-web/components/issues/issue-modal";
import { IssueTypeSelect, WorkItemTemplateSelect } from "@/plane-web/components/issues/issue-modal";
import { WorkItemModalAdditionalProperties } from "@/plane-web/components/issues/issue-modal/modal-additional-properties";
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
export interface IssueFormProps {
@ -124,6 +121,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
} = useIssueModal();
const { isMobile } = usePlatformOS();
const { moveIssue } = useWorkspaceDraftIssues();
const { createLabel } = useLabel();
const {
issue: { getIssueById },
@ -362,9 +360,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<FormProvider {...methods}>
{projectId && (
<CreateLabelModal
createLabel={createLabel.bind(createLabel, workspaceSlug?.toString(), projectId)}
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId}
onSuccess={(response) => {
setValue<"label_ids">("label_ids", [...watch("label_ids"), response.id]);
handleFormChange();
@ -474,25 +472,19 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onClose={onClose}
/>
</div>
<div
className={cn(
"px-5",
activeAdditionalPropertiesLength <= 4 &&
"max-h-[25vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm"
)}
>
{projectId && (
<IssueAdditionalProperties
issueId={data?.id ?? data?.sourceIssueId}
issueTypeId={watch("type_id")}
projectId={projectId}
workspaceSlug={workspaceSlug?.toString()}
isDraft={isDraft}
/>
)}
</div>
<WorkItemModalAdditionalProperties
isDraft={isDraft}
workItemId={data?.id ?? data?.sourceIssueId}
projectId={projectId}
workspaceSlug={workspaceSlug?.toString()}
/>
</div>
<div className="px-4 py-3 border-t-[0.5px] border-custom-border-200 shadow-custom-shadow-xs rounded-b-lg bg-custom-background-100">
<div
className={cn(
"px-4 py-3 border-t-[0.5px] border-custom-border-200 rounded-b-lg bg-custom-background-100",
activeAdditionalPropertiesLength > 0 && "shadow-custom-shadow-xs"
)}
>
<div className="pb-3 border-b-[0.5px] border-custom-border-200">
<IssueDefaultProperties
control={control}

View File

@ -1,77 +1,76 @@
import React, { Fragment, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Check, Component, Plus, Search, Tag } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
// components
// plane imports
import { IIssueLabel } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { IssueLabelsList } from "@/components/ui";
// helpers
// hooks
import { useLabel } from "@/hooks/store";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
value: string[];
onChange: (value: string[]) => void;
projectId: string | undefined;
label?: JSX.Element;
disabled?: boolean;
tabIndex?: number;
createLabelEnabled?: boolean;
buttonContainerClassName?: string;
export type TWorkItemLabelSelectBaseProps = {
buttonClassName?: string;
buttonContainerClassName?: string;
createLabelEnabled?: boolean;
disabled?: boolean;
getLabelById: (labelId: string) => IIssueLabel | null;
label?: JSX.Element;
labelIds: string[];
onChange: (value: string[]) => void;
onDropdownOpen?: () => void;
placement?: Placement;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
tabIndex?: number;
value: string[];
};
export const IssueLabelSelect: React.FC<Props> = observer((props) => {
export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> = observer((props) => {
const {
setIsOpen,
value,
onChange,
projectId,
label,
disabled = false,
tabIndex,
createLabelEnabled = false,
buttonContainerClassName,
buttonClassName,
buttonContainerClassName,
createLabelEnabled = false,
disabled = false,
getLabelById,
label,
labelIds,
onChange,
onDropdownOpen,
placement,
setIsOpen,
tabIndex,
value,
} = props;
const { t } = useTranslation();
// router
const { workspaceSlug } = useParams();
// store hooks
const { getProjectLabels, fetchProjectLabels } = useLabel();
const { isMobile } = usePlatformOS();
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// states
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper
// plane hooks
const { t } = useTranslation();
// store hooks
const { isMobile } = usePlatformOS();
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
});
const projectLabels = getProjectLabels(projectId);
// derived values
const labelsList = labelIds.map((labelId) => getLabelById(labelId)).filter((label) => !!label);
const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
query === "" ? labelsList : labelsList?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
const onOpen = () => {
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus();
onDropdownOpen?.();
};
const handleClose = () => {
@ -131,7 +130,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
) : value && value.length > 0 ? (
<span className={cn("flex items-center justify-center gap-2 text-xs h-full", buttonClassName)}>
<IssueLabelsList
labels={value.map((v) => projectLabels?.find((l) => l.id === v)) ?? []}
labels={value.map((v) => labelsList?.find((l) => l.id === v)) ?? []}
length={3}
showLength
/>
@ -169,10 +168,10 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{projectLabels && filteredOptions ? (
{labelsList && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = projectLabels?.filter((l) => l.parent === label.id);
const children = labelsList?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)

View File

@ -0,0 +1,35 @@
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks
import { useLabel } from "@/hooks/store";
// local imports
import { TWorkItemLabelSelectBaseProps, WorkItemLabelSelectBase } from "./base";
type TWorkItemLabelSelectProps = Omit<TWorkItemLabelSelectBaseProps, "labelIds" | "getLabelById" | "onDropdownOpen"> & {
projectId: string | undefined;
};
export const IssueLabelSelect: React.FC<TWorkItemLabelSelectProps> = observer((props) => {
const { projectId } = props;
// router
const { workspaceSlug } = useParams();
// store hooks
const { getProjectLabelIds, getLabelById, fetchProjectLabels } = useLabel();
// derived values
const projectLabelIds = getProjectLabelIds(projectId);
const onDropdownOpen = () => {
if (projectLabelIds === undefined && workspaceSlug && projectId)
fetchProjectLabels(workspaceSlug.toString(), projectId);
};
return (
<WorkItemLabelSelectBase
{...props}
getLabelById={getLabelById}
labelIds={projectLabelIds ?? []}
onDropdownOpen={onDropdownOpen}
/>
);
});

View File

@ -1 +1 @@
export * from "./label";
export * from "./dropdown";

View File

@ -2,7 +2,6 @@
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { TwitterPicker } from "react-color";
import { Controller, useForm } from "react-hook-form";
import { ChevronDown } from "lucide-react";
@ -16,14 +15,14 @@ import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { getTabIndex } from "@plane/utils";
// hooks
import { useLabel } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
isOpen: boolean;
projectId: string;
createLabel: (data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
handleClose: () => void;
isOpen: boolean;
onSuccess?: (response: IIssueLabel) => void;
};
@ -33,11 +32,8 @@ const defaultValues: Partial<IState> = {
};
export const CreateLabelModal: React.FC<Props> = observer((props) => {
const { isOpen, projectId, handleClose, onSuccess } = props;
// router
const { workspaceSlug } = useParams();
const { createLabel, handleClose, isOpen, onSuccess } = props;
// store hooks
const { createLabel } = useLabel();
const { isMobile } = usePlatformOS();
// form info
const {
@ -71,9 +67,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
};
const onSubmit = async (formData: IIssueLabel) => {
if (!workspaceSlug) return;
await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
await createLabel(formData)
.then((res) => {
onClose();
if (onSuccess) onSuccess(res);

View File

@ -93,7 +93,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]}
renderCondition={(projectId) => !!projectsWithCreatePermissions?.[projectId]}
tabIndex={getIndex("cover_image")}
/>
</div>

View File

@ -0,0 +1,27 @@
import { FieldError, FieldValues } from "react-hook-form";
/**
* Get a nested error from a form's errors object
* @param errors - The form's errors object
* @param path - The path to the error
* @returns The error or undefined if not found
*/
export const getNestedError = <T extends FieldValues>(errors: T, path: string): FieldError | undefined => {
const keys = path.split(".");
let current: unknown = errors;
for (const key of keys) {
if (current && typeof current === "object" && key in current) {
current = (current as Record<string, unknown>)[key];
} else {
return undefined;
}
}
// Check if the final value is a FieldError
if (current && typeof current === "object" && "message" in current) {
return current as FieldError;
}
return undefined;
};

View File

@ -337,21 +337,17 @@ export const joinUrlPath = (...segments: string[]): string => {
if (validSegments.length === 0) return "";
// Process segments to normalize slashes
const processedSegments = validSegments.map((segment, index) => {
const processedSegments = validSegments.map((segment) => {
let processed = segment;
// Remove leading slashes from all segments except the first
if (index > 0) {
while (processed.startsWith("/")) {
processed = processed.substring(1);
}
while (processed.startsWith("/")) {
processed = processed.substring(1);
}
// Remove trailing slashes from all segments except the last
if (index < validSegments.length - 1) {
while (processed.endsWith("/")) {
processed = processed.substring(0, processed.length - 1);
}
while (processed.endsWith("/")) {
processed = processed.substring(0, processed.length - 1);
}
return processed;