[WEB-575] chore: safely re-enable SWR (#3805)

* safley enable swr and make sure to minimalize re renders

* resolve build errors

* fix dropdowns updation by adding observer
This commit is contained in:
rahulramesha 2024-02-28 15:18:11 +05:30 committed by GitHub
parent bd142989b4
commit 6c70d3854a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 952 additions and 1173 deletions

View File

@ -49,8 +49,10 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
const [query, setQuery] = useState("");
// fetching project issues.
const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId && isOpen ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId && isOpen
? () => issueService.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { setToastAlert } = useToast();

View File

@ -1,277 +0,0 @@
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons
import { ContrastIcon, CycleGroupIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
import { TCycleGroups } from "@plane/types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onChange: (val: string | null) => void;
onClose?: () => void;
projectId: string;
value: string | null;
};
type DropdownOptions =
| {
value: string | null;
query: string;
content: JSX.Element;
}[]
| undefined;
export const CycleDropdown: React.FC<Props> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
onChange,
onClose,
placeholder = "Cycle",
placement,
projectId,
showTooltip = false,
tabIndex,
value,
} = 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);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => {
const cycleDetails = getCycleById(cycleId);
return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true;
});
const options: DropdownOptions = cycleIds?.map((cycleId) => {
const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
return {
value: cycleId,
query: `${cycleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<span className="flex-grow truncate">{cycleDetails?.name}</span>
</div>
),
};
});
options?.unshift({
value: null,
query: "No cycle",
content: (
<div className="flex items-center gap-2">
<ContrastIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No cycle</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const selectedCycle = value ? getCycleById(value) : null;
const onOpen = () => {
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId);
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string | null) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Cycle"
tooltipContent={selectedCycle?.name ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate max-w-40">{selectedCycle?.name ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matches found</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
});

View File

@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { observer } from "mobx-react";
//components
import { ContrastIcon, CycleGroupIcon } from "@plane/ui";
//store
import { useApplication, useCycle } from "hooks/store";
//hooks
import { usePopper } from "react-popper";
//icon
import { Check, Search } from "lucide-react";
//types
import { Placement } from "@popperjs/core";
import { TCycleGroups } from "@plane/types";
type DropdownOptions =
| {
value: string | null;
query: string;
content: JSX.Element;
}[]
| undefined;
interface Props {
projectId: string;
referenceElement: HTMLButtonElement | null;
placement: Placement | undefined;
isOpen: boolean;
}
export const CycleOptions = observer((props: any) => {
const { projectId, isOpen, referenceElement, placement } = props;
//state hooks
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => {
const cycleDetails = getCycleById(cycleId);
return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true;
});
const onOpen = () => {
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId);
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options: DropdownOptions = cycleIds?.map((cycleId) => {
const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
return {
value: cycleId,
query: `${cycleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<span className="flex-grow truncate">{cycleDetails?.name}</span>
</div>
),
};
});
options?.unshift({
value: null,
query: "No cycle",
content: (
<div className="flex items-center gap-2">
<ContrastIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No cycle</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matches found</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
);
});

View File

@ -0,0 +1,149 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { ChevronDown } from "lucide-react";
// hooks
import { useCycle } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "../buttons";
// icons
import { ContrastIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "../types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
import { CycleOptions } from "./cycle-options";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onChange: (val: string | null) => void;
onClose?: () => void;
projectId: string;
value: string | null;
};
export const CycleDropdown: React.FC<Props> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
onChange,
onClose,
placeholder = "Cycle",
placement,
projectId,
showTooltip = false,
tabIndex,
value,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
const { getCycleNameById } = useCycle();
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const selectedName = value ? getCycleNameById(value) : null;
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
};
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string | null) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Cycle"
tooltipContent={selectedName ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate max-w-40">{selectedName ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<CycleOptions isOpen={isOpen} projectId={projectId} placement={placement} referenceElement={referenceElement} />
)}
</Combobox>
);
});

View File

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

View File

@ -0,0 +1,156 @@
import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { ChevronDown } from "lucide-react";
// hooks
import { useMember } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
import { MemberOptions } from "./member-options";
type Props = {
projectId?: string;
onClose?: () => void;
} & MemberDropdownProps;
export const MemberDropdown: React.FC<Props> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Members",
placement,
projectId,
showTooltip = false,
tabIndex,
value,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const { getUserDetails } = useMember();
const comboboxProps: any = {
value,
onChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
};
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-xs leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<MemberOptions
isOpen={isOpen}
projectId={projectId}
placement={placement}
referenceElement={referenceElement}
/>
)}
</Combobox>
);
});

View File

@ -0,0 +1,142 @@
import { useEffect, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { observer } from "mobx-react";
//components
import { Avatar } from "@plane/ui";
//store
import { useApplication, useMember, useUser } from "hooks/store";
//hooks
import { usePopper } from "react-popper";
//icon
import { Check, Search } from "lucide-react";
//types
import { Placement } from "@popperjs/core";
interface Props {
projectId?: string;
referenceElement: HTMLButtonElement | null;
placement: Placement | undefined;
isOpen: boolean;
}
export const MemberOptions = observer((props: Props) => {
const { projectId, referenceElement, placement, isOpen } = props;
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const {
getUserDetails,
project: { getProjectMemberIds, fetchProjectMembers },
workspace: { workspaceMemberIds },
} = useMember();
const { currentUser } = useUser();
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
const memberIds = projectId ? getProjectMemberIds(projectId) : workspaceMemberIds;
const onOpen = () => {
if (!memberIds && workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId);
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options = memberIds?.map((userId) => {
const userDetails = getUserDetails(userId);
return {
value: userId,
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
content: (
<div className="flex items-center gap-2">
<Avatar name={userDetails?.display_name} src={userDetails?.avatar} />
<span className="flex-grow truncate">{currentUser?.id === userId ? "You" : userDetails?.display_name}</span>
</div>
),
};
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
);
});

View File

@ -1,261 +0,0 @@
import { Fragment, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
type Props = {
projectId: string;
onClose?: () => void;
} & MemberDropdownProps;
export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Members",
placement,
projectId,
showTooltip = false,
tabIndex,
value,
} = 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);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { currentUser } = useUser();
const {
getUserDetails,
project: { getProjectMemberIds, fetchProjectMembers },
} = useMember();
const projectMemberIds = getProjectMemberIds(projectId);
const options = projectMemberIds?.map((userId) => {
const userDetails = getUserDetails(userId);
return {
value: userId,
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
content: (
<div className="flex items-center gap-2">
<Avatar name={userDetails?.display_name} src={userDetails?.avatar} />
<span className="flex-grow truncate">{currentUser?.id === userId ? "You" : userDetails?.display_name}</span>
</div>
),
};
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const comboboxProps: any = {
value,
onChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
const onOpen = () => {
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-xs leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
});

View File

@ -1,238 +0,0 @@
import { Fragment, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
const {
button,
buttonClassName,
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Members",
placement,
showTooltip = false,
tabIndex,
value,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState<boolean>(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);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const { currentUser } = useUser();
const {
getUserDetails,
workspace: { workspaceMemberIds },
} = useMember();
const options = workspaceMemberIds?.map((userId) => {
const userDetails = getUserDetails(userId);
return {
value: userId,
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
content: (
<div className="flex items-center gap-2">
<Avatar name={userDetails?.display_name} src={userDetails?.avatar} />
<span className="flex-grow truncate">{currentUser?.id === userId ? "You" : userDetails?.display_name}</span>
</div>
),
};
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const comboboxProps: any = {
value,
onChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
};
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
{...comboboxProps}
handleKeyDown={handleKeyDown}
onChange={dropdownOnChange}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-xs leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
});

View File

@ -1,22 +1,22 @@
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search, X } from "lucide-react";
import { ChevronDown, X } from "lucide-react";
// hooks
import { useApplication, useModule } from "hooks/store";
import { useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
import { DropdownButton } from "../buttons";
// icons
import { DiceIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
import { TDropdownProps } from "../types";
// constants
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants";
import { ModuleOptions } from "./module-options";
type Props = TDropdownProps & {
button?: ReactNode;
@ -38,14 +38,6 @@ type Props = TDropdownProps & {
}
);
type DropdownOptions =
| {
value: string | null;
query: string;
content: JSX.Element;
}[]
| undefined;
type ButtonContentProps = {
disabled: boolean;
dropdownArrow: boolean;
@ -166,64 +158,14 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
value,
} = 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);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
const moduleIds = getProjectModuleIds(projectId);
const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
value: moduleId,
query: `${moduleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{moduleDetails?.name}</span>
</div>
),
};
});
if (!multiple)
options?.unshift({
value: null,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const onOpen = () => {
if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId);
};
const { getModuleNameById } = useModule();
const handleClose = () => {
if (!isOpen) return;
@ -232,7 +174,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
@ -249,13 +190,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
toggleDropdown();
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose);
const comboboxProps: any = {
@ -314,7 +248,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
tooltipContent={
Array.isArray(value)
? `${value
.map((moduleId) => getModuleById(moduleId)?.name)
.map((moduleId) => getModuleNameById(moduleId))
.toString()
.replaceAll(",", ", ")}`
: ""
@ -339,61 +273,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
<ModuleOptions
isOpen={isOpen}
projectId={projectId}
placement={placement}
referenceElement={referenceElement}
multiple={multiple}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
}
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);

View File

@ -0,0 +1,163 @@
import { useEffect, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { observer } from "mobx-react";
//components
import { DiceIcon } from "@plane/ui";
//store
import { useApplication, useModule } from "hooks/store";
//hooks
import { usePopper } from "react-popper";
import { cn } from "helpers/common.helper";
//icon
import { Check, Search } from "lucide-react";
//types
import { Placement } from "@popperjs/core";
type DropdownOptions =
| {
value: string | null;
query: string;
content: JSX.Element;
}[]
| undefined;
interface Props {
projectId: string;
referenceElement: HTMLButtonElement | null;
placement: Placement | undefined;
isOpen: boolean;
multiple: boolean;
}
export const ModuleOptions = observer((props: Props) => {
const { projectId, isOpen, referenceElement, placement, multiple } = props;
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const moduleIds = getProjectModuleIds(projectId);
const onOpen = () => {
if (workspaceSlug && !moduleIds) fetchModules(workspaceSlug, projectId);
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
value: moduleId,
query: `${moduleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{moduleDetails?.name}</span>
</div>
),
};
});
if (!multiple)
options?.unshift({
value: null,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
}
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
);
});

View File

@ -21,10 +21,10 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
MemberDropdown,
ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
StateDropdown,
} from "components/dropdowns";
// ui
@ -474,7 +474,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectMemberDropdown
<MemberDropdown
projectId={projectId}
value={value}
onChange={onChange}

View File

@ -5,7 +5,7 @@ import { CalendarCheck2, Signal, Tag } from "lucide-react";
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
// components
import { IssueLabel, TIssueOperations } from "components/issues";
import { DateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// icons
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
// helper
@ -80,7 +80,7 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
</div>
<ProjectMemberDropdown
<MemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable}

View File

@ -27,13 +27,7 @@ import {
IssueLabel,
} from "components/issues";
import { IssueSubscription } from "./subscription";
import {
DateDropdown,
EstimateDropdown,
PriorityDropdown,
ProjectMemberDropdown,
StateDropdown,
} from "components/dropdowns";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// icons
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui";
// helpers
@ -161,7 +155,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
</div>
<ProjectMemberDropdown
<MemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable}

View File

@ -26,7 +26,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { getProjectById } = useProject();
const { getProjectIdentifierById } = useProject();
const { getProjectStates } = useProjectState();
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
@ -108,7 +108,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="truncate text-xs">{issue.name}</div>

View File

@ -66,7 +66,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issueId } = props;
// store hooks
const { getStateById } = useProjectState();
const { getProjectById } = useProject();
const { getProjectIdentifierById } = useProject();
const {
router: { workspaceSlug },
} = useApplication();
@ -76,7 +76,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
} = useIssueDetail();
// derived values
const issueDetails = getIssueById(issueId);
const projectDetails = issueDetails && getProjectById(issueDetails?.project_id);
const projectIdentifier = issueDetails && getProjectIdentifierById(issueDetails?.project_id);
const stateDetails = issueDetails && getStateById(issueDetails?.state_id);
const handleIssuePeekOverview = () =>
@ -95,7 +95,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
<div className="relative flex h-full w-full cursor-pointer items-center gap-2">
{stateDetails && <StateGroupIcon stateGroup={stateDetails?.group} color={stateDetails?.color} />}
<div className="flex-shrink-0 text-xs text-custom-text-300">
{projectDetails?.identifier} {issueDetails?.sequence_id}
{projectIdentifier} {issueDetails?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>

View File

@ -42,9 +42,9 @@ interface IssueDetailsBlockProps {
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
// hooks
const { getProjectById } = useProject();
const { getProjectIdentifierById } = useProject();
const {
router: { workspaceSlug, projectId },
router: { workspaceSlug },
} = useApplication();
const { setPeekIssue } = useIssueDetail();
@ -64,7 +64,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
<div className="relative">
<div className="line-clamp-1 text-xs text-custom-text-300">
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
{getProjectIdentifierById(issue.project_id)}-{issue.sequence_id}
</div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
</div>

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
@ -46,7 +46,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
const isCompletedCycle =
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
const canEditIssueProperties = () => !isCompletedCycle;
const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]);
const addIssuesToView = useCallback(
(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
},
[issues?.addIssueToCycle, workspaceSlug, projectId, cycleId]
);
return (
<BaseKanBanRoot
@ -57,10 +65,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
QuickActions={CycleIssueQuickActions}
viewId={cycleId?.toString() ?? ""}
storeType={EIssuesStoreType.CYCLE}
addIssuesToView={(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
}}
addIssuesToView={addIssuesToView}
canEditPropertiesBasedOnProject={canEditIssueProperties}
isCompletedCycle={isCompletedCycle}
/>

View File

@ -24,9 +24,9 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props;
// hooks
const {
router: { workspaceSlug, projectId },
router: { workspaceSlug },
} = useApplication();
const { getProjectById } = useProject();
const { getProjectIdentifierById } = useProject();
const { peekIssue, setPeekIssue } = useIssueDetail();
const updateIssue = async (issueToUpdate: TIssue) => {
@ -45,7 +45,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id);
const projectDetails = getProjectById(issue.project_id);
const projectIdentifier = getProjectIdentifierById(issue.project_id);
return (
<div
@ -56,7 +56,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
>
{displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{projectDetails?.identifier}-{issue.sequence_id}
{projectIdentifier}-{issue.sequence_id}
</div>
)}

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
@ -44,7 +44,15 @@ export const CycleListLayout: React.FC = observer(() => {
const isCompletedCycle =
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
const canEditIssueProperties = () => !isCompletedCycle;
const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]);
const addIssuesToView = useCallback(
(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
},
[issues?.addIssueToCycle, workspaceSlug, projectId, cycleId]
);
return (
<BaseListRoot
@ -54,10 +62,7 @@ export const CycleListLayout: React.FC = observer(() => {
issueActions={issueActions}
viewId={cycleId?.toString()}
storeType={EIssuesStoreType.CYCLE}
addIssuesToView={(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
}}
addIssuesToView={addIssuesToView}
canEditPropertiesBasedOnProject={canEditIssueProperties}
isCompletedCycle={isCompletedCycle}
/>

View File

@ -13,7 +13,7 @@ import {
DateDropdown,
EstimateDropdown,
PriorityDropdown,
ProjectMemberDropdown,
MemberDropdown,
ModuleDropdown,
CycleDropdown,
StateDropdown,
@ -313,7 +313,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* assignee */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
<div className="h-5">
<ProjectMemberDropdown
<MemberDropdown
projectId={issue?.project_id}
value={issue?.assignee_ids}
onChange={handleAssignee}

View File

@ -88,11 +88,15 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
}
};
useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => {
useSWR(
workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null,
async () => {
if (workspaceSlug) {
await fetchAllGlobalViews(workspaceSlug.toString());
}
});
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
useSWR(
workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null,
@ -103,7 +107,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader");
routerFilterParams();
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const canEditProperties = useCallback(

View File

@ -33,7 +33,8 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {

View File

@ -47,7 +47,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
cycleId.toString()
);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;

View File

@ -33,7 +33,8 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;

View File

@ -43,7 +43,8 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
moduleId.toString()
);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const userFilters = issuesFilter?.issueFilters?.filters;

View File

@ -29,7 +29,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, async () => {
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
@ -38,7 +40,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
}
});
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;

View File

@ -41,7 +41,8 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
viewId.toString()
);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueActions = useMemo(

View File

@ -1,7 +1,7 @@
import React from "react";
import { observer } from "mobx-react-lite";
// components
import { ProjectMemberDropdown } from "components/dropdowns";
import { MemberDropdown } from "components/dropdowns";
// types
import { TIssue } from "@plane/types";
@ -17,7 +17,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props
return (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<ProjectMemberDropdown
<MemberDropdown
value={issue?.assignee_ids ?? []}
onChange={(data) => {
onChange(

View File

@ -142,7 +142,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const router = useRouter();
const { workspaceSlug } = router.query;
//hooks
const { getProjectById } = useProject();
const { getProjectIdentifierById } = useProject();
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
@ -212,7 +212,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
isMenuActive ? "opacity-0" : "opacity-100"
}`}
>
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
{getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id}
</span>
{canEditProperties(issueDetail.project_id) && (

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx store
@ -39,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
const isCompletedCycle =
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
const canEditIssueProperties = () => !isCompletedCycle;
const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]);
return (
<BaseSpreadsheetRoot

View File

@ -23,7 +23,7 @@ import {
ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
MemberDropdown,
StateDropdown,
} from "components/dropdowns";
// ui
@ -510,7 +510,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectMemberDropdown
<MemberDropdown
projectId={projectId}
value={value}
onChange={(assigneeIds) => {

View File

@ -54,7 +54,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const {
router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const { workspaceProjectIds } = useProject();
const { fetchCycleDetails } = useCycle();
const { fetchModuleDetails } = useModule();

View File

@ -14,13 +14,7 @@ import {
TIssueOperations,
IssueRelationSelect,
} from "components/issues";
import {
DateDropdown,
EstimateDropdown,
PriorityDropdown,
ProjectMemberDropdown,
StateDropdown,
} from "components/dropdowns";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// components
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// helpers
@ -87,7 +81,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
</div>
<ProjectMemberDropdown
<MemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={disabled}

View File

@ -2,7 +2,7 @@ import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
// components
import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// types
import { TSubIssueOperations } from "./root";
@ -62,7 +62,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
</div>
<div className="h-5 flex-shrink-0">
<ProjectMemberDropdown
<MemberDropdown
value={issue.assignee_ids}
projectId={issue.project_id}
onChange={(val) =>

View File

@ -1,7 +1,6 @@
export * from "./create-label-modal";
export * from "./create-update-label-inline";
export * from "./delete-label-modal";
export * from "./labels-list-modal";
export * from "./project-setting-label-group";
export * from "./project-setting-label-item";
export * from "./project-setting-label-list";

View File

@ -1,157 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Search } from "lucide-react";
// hooks
import { useLabel } from "hooks/store";
// icons
import { LayerStackIcon } from "@plane/ui";
// types
import { IIssueLabel } from "@plane/types";
type Props = {
isOpen: boolean;
handleClose: () => void;
parent: IIssueLabel | undefined;
};
export const LabelsListModal: React.FC<Props> = observer((props) => {
const { isOpen, handleClose, parent } = props;
// states
const [query, setQuery] = useState("");
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { projectLabels, fetchProjectLabels, updateLabel } = useLabel();
// api call to fetch project details
useSWR(
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
);
// derived values
const filteredLabels: IIssueLabel[] =
query === ""
? projectLabels ?? []
: projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
const handleModalClose = () => {
handleClose();
setQuery("");
};
const addChildLabel = async (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return;
await updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
parent: parent?.id!,
});
};
return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all">
<Combobox>
<div className="relative m-1">
<Search
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
{filteredLabels.length > 0 && (
<li className="p-2">
{query === "" && (
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Labels</h2>
)}
<ul className="text-sm text-gray-700">
{filteredLabels.map((label) => {
const children = projectLabels?.filter((l) => l.parent === label.id);
if (
(label.parent === "" || label.parent === null) && // issue does not have any other parent
label.id !== parent?.id && // issue is not itself
children?.length === 0 // issue doesn't have any other children
)
return (
<Combobox.Option
key={label.id}
value={{
name: label.name,
}}
className={({ active }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
}`
}
onClick={() => {
addChildLabel(label);
}}
>
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
</Combobox.Option>
);
})}
</ul>
</li>
)}
</Combobox.Options>
{query !== "" && filteredLabels.length === 0 && (
<div className="px-6 py-14 text-center sm:px-14">
<LayerStackIcon
className="mx-auto h-6 w-6 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
<p className="mt-4 text-sm text-custom-text-100">
We couldn{"'"}t find any label with that term. Please try again.
</p>
</div>
)}
</Combobox>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
});

View File

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// components
import { ModuleStatusSelect } from "components/modules";
import { DateRangeDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns";
import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/dropdowns";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
@ -175,7 +175,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
name="lead_id"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectMemberDropdown
<MemberDropdown
value={value}
onChange={onChange}
projectId={projectId}
@ -192,7 +192,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
name="member_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectMemberDropdown
<MemberDropdown
value={value}
onChange={onChange}
projectId={projectId}

View File

@ -21,7 +21,7 @@ import useToast from "hooks/use-toast";
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart";
import { DateRangeDropdown, ProjectMemberDropdown } from "components/dropdowns";
import { DateRangeDropdown, MemberDropdown } from "components/dropdowns";
// ui
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
@ -385,7 +385,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="lead_id"
render={({ field: { value } }) => (
<div className="w-3/5 h-7">
<ProjectMemberDropdown
<MemberDropdown
value={value ?? null}
onChange={(val) => {
submitChanges({ lead_id: val });
@ -409,7 +409,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="member_ids"
render={({ field: { value } }) => (
<div className="w-3/5 h-7">
<ProjectMemberDropdown
<MemberDropdown
value={value ?? []}
onChange={(val: string[]) => {
submitChanges({ member_ids: val });

View File

@ -51,7 +51,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
await fetchFilters(workspaceSlug, userId);
await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type);
}
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";

View File

@ -11,7 +11,7 @@ import { Button, CustomSelect, Input, TextArea } from "@plane/ui";
// components
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
import { WorkspaceMemberDropdown } from "components/dropdowns";
import { MemberDropdown } from "components/dropdowns";
// helpers
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// constants
@ -383,7 +383,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
control={control}
render={({ field: { value, onChange } }) => (
<div className="h-7 flex-shrink-0" tabIndex={5}>
<WorkspaceMemberDropdown
<MemberDropdown
value={value}
onChange={onChange}
placeholder="Lead"

View File

@ -1,7 +1,7 @@
export const SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnMount: true,
refreshInterval: 600000,
errorRetryCount: 3,

View File

@ -15,30 +15,35 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
// fetch workspace Modules
useSWR(
workspaceSlug ? `WORKSPACE_MODULES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace Cycles
useSWR(
workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace labels
useSWR(
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace states
useSWR(
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace estimates
useSWR(
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
};

View File

@ -33,7 +33,7 @@ export const AppLayout: FC<IAppLayout> = observer((props) => {
// await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
}
},
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
{ revalidateIfStale: false, revalidateOnFocus: false }
);
return (

View File

@ -66,37 +66,44 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// fetching project labels
useSWR(
workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project members
useSWR(
workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project states
useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project estimates
useSWR(
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project cycles
useSWR(
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project modules
useSWR(
workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project views
useSWR(
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null
workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching project inboxes if inbox is enabled in project settings
useSWR(

View File

@ -26,22 +26,26 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
// fetching user workspace information
useSWR(
workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null
workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetching workspace projects
useSWR(
workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace members
useSWR(
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace user projects role
useSWR(
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null
workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// while data is being loaded

View File

@ -28,6 +28,7 @@ export interface ICycleStore {
currentProjectActiveCycleId: string | null;
// computed actions
getCycleById: (cycleId: string) => ICycle | null;
getCycleNameById: (cycleId: string) => string | undefined;
getActiveCycleById: (cycleId: string) => ICycle | null;
getProjectCycleIds: (projectId: string) => string[] | null;
// actions
@ -189,6 +190,13 @@ export class CycleStore implements ICycleStore {
*/
getCycleById = computedFn((cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null);
/**
* @description returns cycle name by cycle id
* @param cycleId
* @returns
*/
getCycleNameById = computedFn((cycleId: string): string => this.cycleMap?.[cycleId]?.name);
/**
* @description returns active cycle details by cycle id
* @param cycleId

View File

@ -19,6 +19,7 @@ export interface IModuleStore {
projectModuleIds: string[] | null;
// computed actions
getModuleById: (moduleId: string) => IModule | null;
getModuleNameById: (moduleId: string) => string;
getProjectModuleIds: (projectId: string) => string[] | null;
// actions
// fetch
@ -114,6 +115,13 @@ export class ModulesStore implements IModuleStore {
*/
getModuleById = computedFn((moduleId: string) => this.moduleMap?.[moduleId] || null);
/**
* @description get module by id
* @param moduleId
* @returns IModule | null
*/
getModuleNameById = computedFn((moduleId: string) => this.moduleMap?.[moduleId]?.name);
/**
* @description returns list of module ids of the project id passed as argument
* @param projectId

View File

@ -24,6 +24,7 @@ export interface IProjectStore {
// actions
setSearchQuery: (query: string) => void;
getProjectById: (projectId: string) => IProject | null;
getProjectIdentifierById: (projectId: string) => string;
// fetch actions
fetchProjects: (workspaceSlug: string) => Promise<IProject[]>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -210,6 +211,16 @@ export class ProjectStore implements IProjectStore {
return projectInfo;
});
/**
* Returns project identifier using project id
* @param projectId
* @returns string
*/
getProjectIdentifierById = computedFn((projectId: string) => {
const projectInfo = this.projectMap?.[projectId];
return projectInfo?.identifier;
});
/**
* Adds project to favorites and updates project favorite status in the store
* @param workspaceSlug