forked from github/plane
[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:
parent
bd142989b4
commit
6c70d3854a
@ -49,8 +49,10 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
// fetching project issues.
|
// fetching project issues.
|
||||||
const { data: issues } = useSWR(
|
const { data: issues } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
workspaceSlug && projectId && isOpen ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||||
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
|
workspaceSlug && projectId && isOpen
|
||||||
|
? () => issueService.getIssues(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
162
web/components/dropdowns/cycle/cycle-options.tsx
Normal file
162
web/components/dropdowns/cycle/cycle-options.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
149
web/components/dropdowns/cycle/index.tsx
Normal file
149
web/components/dropdowns/cycle/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./project-member";
|
|
||||||
export * from "./workspace-member";
|
|
156
web/components/dropdowns/member/index.tsx
Normal file
156
web/components/dropdowns/member/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
142
web/components/dropdowns/member/member-options.tsx
Normal file
142
web/components/dropdowns/member/member-options.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,22 +1,22 @@
|
|||||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { ChevronDown, X } from "lucide-react";
|
||||||
import { Check, ChevronDown, Search, X } from "lucide-react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useModule } from "hooks/store";
|
import { useModule } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { DropdownButton } from "./buttons";
|
import { DropdownButton } from "../buttons";
|
||||||
// icons
|
// icons
|
||||||
import { DiceIcon, Tooltip } from "@plane/ui";
|
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "../types";
|
||||||
// constants
|
// constants
|
||||||
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
|
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants";
|
||||||
|
import { ModuleOptions } from "./module-options";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -38,14 +38,6 @@ type Props = TDropdownProps & {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type DropdownOptions =
|
|
||||||
| {
|
|
||||||
value: string | null;
|
|
||||||
query: string;
|
|
||||||
content: JSX.Element;
|
|
||||||
}[]
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
type ButtonContentProps = {
|
type ButtonContentProps = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
dropdownArrow: boolean;
|
dropdownArrow: boolean;
|
||||||
@ -166,64 +158,14 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
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 { getModuleNameById } = useModule();
|
||||||
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 handleClose = () => {
|
const handleClose = () => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
@ -232,7 +174,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
if (!isOpen) onOpen();
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -249,13 +190,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (query !== "" && e.key === "Escape") {
|
|
||||||
e.stopPropagation();
|
|
||||||
setQuery("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
const comboboxProps: any = {
|
const comboboxProps: any = {
|
||||||
@ -314,7 +248,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
tooltipContent={
|
tooltipContent={
|
||||||
Array.isArray(value)
|
Array.isArray(value)
|
||||||
? `${value
|
? `${value
|
||||||
.map((moduleId) => getModuleById(moduleId)?.name)
|
.map((moduleId) => getModuleNameById(moduleId))
|
||||||
.toString()
|
.toString()
|
||||||
.replaceAll(",", ", ")}`
|
.replaceAll(",", ", ")}`
|
||||||
: ""
|
: ""
|
||||||
@ -339,61 +273,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Combobox.Options className="fixed z-10" static>
|
<ModuleOptions
|
||||||
<div
|
isOpen={isOpen}
|
||||||
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"
|
projectId={projectId}
|
||||||
ref={setPopperElement}
|
placement={placement}
|
||||||
style={styles.popper}
|
referenceElement={referenceElement}
|
||||||
{...attributes.popper}
|
multiple={multiple}
|
||||||
>
|
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
);
|
);
|
163
web/components/dropdowns/module/module-options.tsx
Normal file
163
web/components/dropdowns/module/module-options.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -21,10 +21,10 @@ import {
|
|||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
|
MemberDropdown,
|
||||||
ModuleDropdown,
|
ModuleDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
|
||||||
StateDropdown,
|
StateDropdown,
|
||||||
} from "components/dropdowns";
|
} from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
@ -474,7 +474,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
name="assignee_ids"
|
name="assignee_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -5,7 +5,7 @@ import { CalendarCheck2, Signal, Tag } from "lucide-react";
|
|||||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { IssueLabel, TIssueOperations } from "components/issues";
|
import { IssueLabel, TIssueOperations } from "components/issues";
|
||||||
import { DateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
// icons
|
// icons
|
||||||
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helper
|
// helper
|
||||||
@ -80,7 +80,7 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span>Assignees</span>
|
<span>Assignees</span>
|
||||||
</div>
|
</div>
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={issue?.assignee_ids ?? undefined}
|
value={issue?.assignee_ids ?? undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||||
disabled={!is_editable}
|
disabled={!is_editable}
|
||||||
|
@ -27,13 +27,7 @@ import {
|
|||||||
IssueLabel,
|
IssueLabel,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { IssueSubscription } from "./subscription";
|
import { IssueSubscription } from "./subscription";
|
||||||
import {
|
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
DateDropdown,
|
|
||||||
EstimateDropdown,
|
|
||||||
PriorityDropdown,
|
|
||||||
ProjectMemberDropdown,
|
|
||||||
StateDropdown,
|
|
||||||
} from "components/dropdowns";
|
|
||||||
// icons
|
// icons
|
||||||
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui";
|
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -161,7 +155,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span>Assignees</span>
|
<span>Assignees</span>
|
||||||
</div>
|
</div>
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={issue?.assignee_ids ?? undefined}
|
value={issue?.assignee_ids ?? undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||||
disabled={!is_editable}
|
disabled={!is_editable}
|
||||||
|
@ -26,7 +26,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
router: { workspaceSlug, projectId },
|
router: { workspaceSlug, projectId },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
@ -108,7 +108,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<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>
|
</div>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<div className="truncate text-xs">{issue.name}</div>
|
<div className="truncate text-xs">{issue.name}</div>
|
||||||
|
@ -66,7 +66,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
const { issueId } = props;
|
const { issueId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
@ -76,7 +76,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
// derived values
|
// derived values
|
||||||
const issueDetails = getIssueById(issueId);
|
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 stateDetails = issueDetails && getStateById(issueDetails?.state_id);
|
||||||
|
|
||||||
const handleIssuePeekOverview = () =>
|
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">
|
<div className="relative flex h-full w-full cursor-pointer items-center gap-2">
|
||||||
{stateDetails && <StateGroupIcon stateGroup={stateDetails?.group} color={stateDetails?.color} />}
|
{stateDetails && <StateGroupIcon stateGroup={stateDetails?.group} color={stateDetails?.color} />}
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
||||||
{projectDetails?.identifier} {issueDetails?.sequence_id}
|
{projectIdentifier} {issueDetails?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
|
||||||
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
|
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
|
||||||
|
@ -42,9 +42,9 @@ interface IssueDetailsBlockProps {
|
|||||||
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
||||||
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
|
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug, projectId },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { setPeekIssue } = useIssueDetail();
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="line-clamp-1 text-xs text-custom-text-300">
|
<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>
|
||||||
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
|
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
@ -46,7 +46,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
const isCompletedCycle =
|
const isCompletedCycle =
|
||||||
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
|
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 (
|
return (
|
||||||
<BaseKanBanRoot
|
<BaseKanBanRoot
|
||||||
@ -57,10 +65,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
QuickActions={CycleIssueQuickActions}
|
QuickActions={CycleIssueQuickActions}
|
||||||
viewId={cycleId?.toString() ?? ""}
|
viewId={cycleId?.toString() ?? ""}
|
||||||
storeType={EIssuesStoreType.CYCLE}
|
storeType={EIssuesStoreType.CYCLE}
|
||||||
addIssuesToView={(issueIds: string[]) => {
|
addIssuesToView={addIssuesToView}
|
||||||
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
|
|
||||||
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
|
|
||||||
}}
|
|
||||||
canEditPropertiesBasedOnProject={canEditIssueProperties}
|
canEditPropertiesBasedOnProject={canEditIssueProperties}
|
||||||
isCompletedCycle={isCompletedCycle}
|
isCompletedCycle={isCompletedCycle}
|
||||||
/>
|
/>
|
||||||
|
@ -24,9 +24,9 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props;
|
const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug, projectId },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
const updateIssue = async (issueToUpdate: TIssue) => {
|
const updateIssue = async (issueToUpdate: TIssue) => {
|
||||||
@ -45,7 +45,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
|
|
||||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||||
const projectDetails = getProjectById(issue.project_id);
|
const projectIdentifier = getProjectIdentifierById(issue.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -56,7 +56,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
>
|
>
|
||||||
{displayProperties && displayProperties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||||
{projectDetails?.identifier}-{issue.sequence_id}
|
{projectIdentifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
@ -44,7 +44,15 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
const isCompletedCycle =
|
const isCompletedCycle =
|
||||||
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
|
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 (
|
return (
|
||||||
<BaseListRoot
|
<BaseListRoot
|
||||||
@ -54,10 +62,7 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
issueActions={issueActions}
|
issueActions={issueActions}
|
||||||
viewId={cycleId?.toString()}
|
viewId={cycleId?.toString()}
|
||||||
storeType={EIssuesStoreType.CYCLE}
|
storeType={EIssuesStoreType.CYCLE}
|
||||||
addIssuesToView={(issueIds: string[]) => {
|
addIssuesToView={addIssuesToView}
|
||||||
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
|
|
||||||
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
|
|
||||||
}}
|
|
||||||
canEditPropertiesBasedOnProject={canEditIssueProperties}
|
canEditPropertiesBasedOnProject={canEditIssueProperties}
|
||||||
isCompletedCycle={isCompletedCycle}
|
isCompletedCycle={isCompletedCycle}
|
||||||
/>
|
/>
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectMemberDropdown,
|
MemberDropdown,
|
||||||
ModuleDropdown,
|
ModuleDropdown,
|
||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
StateDropdown,
|
StateDropdown,
|
||||||
@ -313,7 +313,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
{/* assignee */}
|
{/* assignee */}
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
|
||||||
<div className="h-5">
|
<div className="h-5">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
projectId={issue?.project_id}
|
projectId={issue?.project_id}
|
||||||
value={issue?.assignee_ids}
|
value={issue?.assignee_ids}
|
||||||
onChange={handleAssignee}
|
onChange={handleAssignee}
|
||||||
|
@ -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) {
|
if (workspaceSlug) {
|
||||||
await fetchAllGlobalViews(workspaceSlug.toString());
|
await fetchAllGlobalViews(workspaceSlug.toString());
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null,
|
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");
|
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader");
|
||||||
routerFilterParams();
|
routerFilterParams();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const canEditProperties = useCallback(
|
const canEditProperties = useCallback(
|
||||||
|
@ -33,7 +33,8 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||||
|
@ -47,7 +47,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||||||
cycleId.toString()
|
cycleId.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
@ -33,7 +33,8 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||||
|
@ -43,7 +43,8 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||||||
moduleId.toString()
|
moduleId.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||||
|
@ -29,7 +29,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
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) {
|
if (workspaceSlug && projectId) {
|
||||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
await issues?.fetchIssues(
|
await issues?.fetchIssues(
|
||||||
@ -38,7 +40,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
|
@ -41,7 +41,8 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
|||||||
viewId.toString()
|
viewId.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const issueActions = useMemo(
|
const issueActions = useMemo(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { ProjectMemberDropdown } from "components/dropdowns";
|
import { MemberDropdown } from "components/dropdowns";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={issue?.assignee_ids ?? []}
|
value={issue?.assignee_ids ?? []}
|
||||||
onChange={(data) => {
|
onChange={(data) => {
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -142,7 +142,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
//hooks
|
//hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
@ -212,7 +212,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
isMenuActive ? "opacity-0" : "opacity-100"
|
isMenuActive ? "opacity-0" : "opacity-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
|
{getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{canEditProperties(issueDetail.project_id) && (
|
{canEditProperties(issueDetail.project_id) && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// mobx store
|
// mobx store
|
||||||
@ -39,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
const isCompletedCycle =
|
const isCompletedCycle =
|
||||||
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
|
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
|
||||||
|
|
||||||
const canEditIssueProperties = () => !isCompletedCycle;
|
const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseSpreadsheetRoot
|
<BaseSpreadsheetRoot
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
ModuleDropdown,
|
ModuleDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
MemberDropdown,
|
||||||
StateDropdown,
|
StateDropdown,
|
||||||
} from "components/dropdowns";
|
} from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
@ -510,7 +510,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
name="assignee_ids"
|
name="assignee_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(assigneeIds) => {
|
onChange={(assigneeIds) => {
|
||||||
|
@ -54,7 +54,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
const {
|
const {
|
||||||
router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId },
|
router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { workspaceProjectIds } = useProject();
|
const { workspaceProjectIds } = useProject();
|
||||||
const { fetchCycleDetails } = useCycle();
|
const { fetchCycleDetails } = useCycle();
|
||||||
const { fetchModuleDetails } = useModule();
|
const { fetchModuleDetails } = useModule();
|
||||||
|
@ -14,13 +14,7 @@ import {
|
|||||||
TIssueOperations,
|
TIssueOperations,
|
||||||
IssueRelationSelect,
|
IssueRelationSelect,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import {
|
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
DateDropdown,
|
|
||||||
EstimateDropdown,
|
|
||||||
PriorityDropdown,
|
|
||||||
ProjectMemberDropdown,
|
|
||||||
StateDropdown,
|
|
||||||
} from "components/dropdowns";
|
|
||||||
// components
|
// components
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// helpers
|
// helpers
|
||||||
@ -87,7 +81,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span>Assignees</span>
|
<span>Assignees</span>
|
||||||
</div>
|
</div>
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={issue?.assignee_ids ?? undefined}
|
value={issue?.assignee_ids ?? undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
// types
|
// types
|
||||||
import { TSubIssueOperations } from "./root";
|
import { TSubIssueOperations } from "./root";
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-5 flex-shrink-0">
|
<div className="h-5 flex-shrink-0">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={issue.assignee_ids}
|
value={issue.assignee_ids}
|
||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export * from "./create-label-modal";
|
export * from "./create-label-modal";
|
||||||
export * from "./create-update-label-inline";
|
export * from "./create-update-label-inline";
|
||||||
export * from "./delete-label-modal";
|
export * from "./delete-label-modal";
|
||||||
export * from "./labels-list-modal";
|
|
||||||
export * from "./project-setting-label-group";
|
export * from "./project-setting-label-group";
|
||||||
export * from "./project-setting-label-item";
|
export * from "./project-setting-label-item";
|
||||||
export * from "./project-setting-label-list";
|
export * from "./project-setting-label-list";
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// components
|
// components
|
||||||
import { ModuleStatusSelect } from "components/modules";
|
import { ModuleStatusSelect } from "components/modules";
|
||||||
import { DateRangeDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns";
|
import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TextArea } from "@plane/ui";
|
import { Button, Input, TextArea } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -175,7 +175,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
|||||||
name="lead_id"
|
name="lead_id"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -192,7 +192,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
|||||||
name="member_ids"
|
name="member_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
@ -21,7 +21,7 @@ import useToast from "hooks/use-toast";
|
|||||||
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
|
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
|
||||||
import { DeleteModuleModal } from "components/modules";
|
import { DeleteModuleModal } from "components/modules";
|
||||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||||
import { DateRangeDropdown, ProjectMemberDropdown } from "components/dropdowns";
|
import { DateRangeDropdown, MemberDropdown } from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
|
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -385,7 +385,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
name="lead_id"
|
name="lead_id"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<div className="w-3/5 h-7">
|
<div className="w-3/5 h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={value ?? null}
|
value={value ?? null}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
submitChanges({ lead_id: val });
|
submitChanges({ lead_id: val });
|
||||||
@ -409,7 +409,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
name="member_ids"
|
name="member_ids"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<div className="w-3/5 h-7">
|
<div className="w-3/5 h-7">
|
||||||
<ProjectMemberDropdown
|
<MemberDropdown
|
||||||
value={value ?? []}
|
value={value ?? []}
|
||||||
onChange={(val: string[]) => {
|
onChange={(val: string[]) => {
|
||||||
submitChanges({ member_ids: val });
|
submitChanges({ member_ids: val });
|
||||||
|
@ -51,7 +51,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
|||||||
await fetchFilters(workspaceSlug, userId);
|
await fetchFilters(workspaceSlug, userId);
|
||||||
await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type);
|
await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||||
|
@ -11,7 +11,7 @@ import { Button, CustomSelect, Input, TextArea } from "@plane/ui";
|
|||||||
// components
|
// components
|
||||||
import { ImagePickerPopover } from "components/core";
|
import { ImagePickerPopover } from "components/core";
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
import { WorkspaceMemberDropdown } from "components/dropdowns";
|
import { MemberDropdown } from "components/dropdowns";
|
||||||
// helpers
|
// helpers
|
||||||
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
||||||
// constants
|
// constants
|
||||||
@ -383,7 +383,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7 flex-shrink-0" tabIndex={5}>
|
<div className="h-7 flex-shrink-0" tabIndex={5}>
|
||||||
<WorkspaceMemberDropdown
|
<MemberDropdown
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="Lead"
|
placeholder="Lead"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export const SWR_CONFIG = {
|
export const SWR_CONFIG = {
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateIfStale: false,
|
revalidateIfStale: true,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: true,
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
refreshInterval: 600000,
|
refreshInterval: 600000,
|
||||||
errorRetryCount: 3,
|
errorRetryCount: 3,
|
||||||
|
@ -15,30 +15,35 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
|
|||||||
// fetch workspace Modules
|
// fetch workspace Modules
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_MODULES_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_MODULES_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch workspace Cycles
|
// fetch workspace Cycles
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch workspace labels
|
// fetch workspace labels
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch workspace states
|
// fetch workspace states
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch workspace estimates
|
// fetch workspace estimates
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@ export const AppLayout: FC<IAppLayout> = observer((props) => {
|
|||||||
// await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
|
// await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -66,37 +66,44 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
// fetching project labels
|
// fetching project labels
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project members
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project states
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project estimates
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project cycles
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project modules
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project views
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project inboxes if inbox is enabled in project settings
|
||||||
useSWR(
|
useSWR(
|
||||||
|
@ -26,22 +26,26 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||||||
// fetching user workspace information
|
// fetching user workspace information
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
|
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
|
// fetching workspace projects
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetch workspace members
|
// fetch workspace members
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
||||||
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetch workspace user projects role
|
// fetch workspace user projects role
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
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
|
// while data is being loaded
|
||||||
|
@ -28,6 +28,7 @@ export interface ICycleStore {
|
|||||||
currentProjectActiveCycleId: string | null;
|
currentProjectActiveCycleId: string | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getCycleById: (cycleId: string) => ICycle | null;
|
getCycleById: (cycleId: string) => ICycle | null;
|
||||||
|
getCycleNameById: (cycleId: string) => string | undefined;
|
||||||
getActiveCycleById: (cycleId: string) => ICycle | null;
|
getActiveCycleById: (cycleId: string) => ICycle | null;
|
||||||
getProjectCycleIds: (projectId: string) => string[] | null;
|
getProjectCycleIds: (projectId: string) => string[] | null;
|
||||||
// actions
|
// actions
|
||||||
@ -189,6 +190,13 @@ export class CycleStore implements ICycleStore {
|
|||||||
*/
|
*/
|
||||||
getCycleById = computedFn((cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null);
|
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
|
* @description returns active cycle details by cycle id
|
||||||
* @param cycleId
|
* @param cycleId
|
||||||
|
@ -19,6 +19,7 @@ export interface IModuleStore {
|
|||||||
projectModuleIds: string[] | null;
|
projectModuleIds: string[] | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getModuleById: (moduleId: string) => IModule | null;
|
getModuleById: (moduleId: string) => IModule | null;
|
||||||
|
getModuleNameById: (moduleId: string) => string;
|
||||||
getProjectModuleIds: (projectId: string) => string[] | null;
|
getProjectModuleIds: (projectId: string) => string[] | null;
|
||||||
// actions
|
// actions
|
||||||
// fetch
|
// fetch
|
||||||
@ -114,6 +115,13 @@ export class ModulesStore implements IModuleStore {
|
|||||||
*/
|
*/
|
||||||
getModuleById = computedFn((moduleId: string) => this.moduleMap?.[moduleId] || null);
|
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
|
* @description returns list of module ids of the project id passed as argument
|
||||||
* @param projectId
|
* @param projectId
|
||||||
|
@ -24,6 +24,7 @@ export interface IProjectStore {
|
|||||||
// actions
|
// actions
|
||||||
setSearchQuery: (query: string) => void;
|
setSearchQuery: (query: string) => void;
|
||||||
getProjectById: (projectId: string) => IProject | null;
|
getProjectById: (projectId: string) => IProject | null;
|
||||||
|
getProjectIdentifierById: (projectId: string) => string;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjects: (workspaceSlug: string) => Promise<IProject[]>;
|
fetchProjects: (workspaceSlug: string) => Promise<IProject[]>;
|
||||||
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
|
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
@ -210,6 +211,16 @@ export class ProjectStore implements IProjectStore {
|
|||||||
return projectInfo;
|
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
|
* Adds project to favorites and updates project favorite status in the store
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
Loading…
Reference in New Issue
Block a user