mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-4457] refactor: decouple work item properties from mobx store (#7363)
This commit is contained in:
parent
5660b28574
commit
f3daac6f95
@ -1,9 +0,0 @@
|
||||
type TIssueAdditionalPropertiesProps = {
|
||||
issueId: string | undefined;
|
||||
issueTypeId: string | null;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
isDraft?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAdditionalProperties: React.FC<TIssueAdditionalPropertiesProps> = () => <></>;
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./provider";
|
||||
export * from "./issue-type-select";
|
||||
export * from "./additional-properties";
|
||||
export * from "./template-select";
|
||||
|
||||
@ -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;
|
||||
@ -44,6 +44,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
handleProjectEntitiesFetch: () => Promise.resolve(),
|
||||
handleTemplateChange: () => Promise.resolve(),
|
||||
handleConvert: () => Promise.resolve(),
|
||||
handleCreateSubWorkItem: () => Promise.resolve(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
48
apps/web/core/components/dropdowns/member/dropdown.tsx
Normal file
48
apps/web/core/components/dropdowns/member/dropdown.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -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}`,
|
||||
|
||||
@ -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>
|
||||
129
apps/web/core/components/dropdowns/module/button-content.tsx
Normal file
129
apps/web/core/components/dropdowns/module/button-content.tsx
Normal 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" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
56
apps/web/core/components/dropdowns/module/dropdown.tsx
Normal file
56
apps/web/core/components/dropdowns/module/dropdown.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -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>) => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
35
apps/web/core/components/dropdowns/project/dropdown.tsx
Normal file
35
apps/web/core/components/dropdowns/project/dropdown.tsx
Normal 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} />;
|
||||
});
|
||||
@ -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" />
|
||||
) : (
|
||||
<>
|
||||
47
apps/web/core/components/dropdowns/state/dropdown.tsx
Normal file
47
apps/web/core/components/dropdowns/state/dropdown.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -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({
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
35
apps/web/core/components/issues/select/dropdown.tsx
Normal file
35
apps/web/core/components/issues/select/dropdown.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -1 +1 @@
|
||||
export * from "./label";
|
||||
export * from "./dropdown";
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
27
apps/web/helpers/react-hook-form.helper.ts
Normal file
27
apps/web/helpers/react-hook-form.helper.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user