forked from github/plane
chore: modal and dropdown improvement (#3332)
* dev: dropdown key down custom hook added * chore: plane ui dropdowns updated * chore: cycle and module tab index added in modals * chore: view and page tab index added in modals * chore: issue modal tab indexing added * chore: project modal tab indexing added * fix: build fix * build-error: build error in pages new structure and reverted back to old page structure --------- Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
parent
0e49d616b7
commit
0f99fb302b
@ -2,6 +2,9 @@ import * as React from "react";
|
||||
|
||||
// react-poppper
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "../hooks/use-outside-click-detector";
|
||||
// headless ui
|
||||
import { Menu } from "@headlessui/react";
|
||||
// type
|
||||
@ -27,16 +30,35 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
verticalEllipsis = false,
|
||||
width = "auto",
|
||||
menuButtonOnClick,
|
||||
tabIndex,
|
||||
} = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
// refs
|
||||
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "auto",
|
||||
});
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Menu as="div" className={`relative w-min text-left ${className}`}>
|
||||
<Menu
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={`relative w-min text-left ${className}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
@ -44,7 +66,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
onClick={() => {
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
className={customButtonClassName}
|
||||
>
|
||||
{customButton}
|
||||
@ -57,7 +82,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={menuButtonOnClick}
|
||||
onClick={() => {
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
disabled={disabled}
|
||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none hover:text-custom-text-100 ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||
@ -78,6 +106,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => {
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && <ChevronDown className="h-3.5 w-3.5" />}
|
||||
@ -86,26 +118,28 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Menu.Items className="fixed z-10">
|
||||
<div
|
||||
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
{isOpen && (
|
||||
<Menu.Items className="fixed z-10" static>
|
||||
<div
|
||||
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "../hooks/use-outside-click-detector";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// icons
|
||||
@ -29,11 +32,15 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
optionsClassName = "",
|
||||
value,
|
||||
width = "auto",
|
||||
tabIndex,
|
||||
} = props;
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
@ -50,8 +57,23 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox as="div" className={`relative flex-shrink-0 text-left ${className}`} {...comboboxProps}>
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={`relative flex-shrink-0 text-left ${className}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...comboboxProps}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open && onOpen) onOpen();
|
||||
|
||||
@ -67,6 +89,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
@ -83,86 +106,89 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
)}
|
||||
<Combobox.Options as={React.Fragment}>
|
||||
<div
|
||||
className={`z-10 my-1 min-w-[10rem] rounded-md border border-custom-border-300 bg-custom-background-90 p-2 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||
} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3 w-3 text-custom-text-200" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent px-2 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="Type to search..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Combobox.Options as={React.Fragment} static>
|
||||
<div
|
||||
className={`mt-2 space-y-1 ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} overflow-y-scroll`}
|
||||
className={`z-10 my-1 min-w-[10rem] rounded-md border border-custom-border-300 bg-custom-background-90 p-2 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||
} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{multiple ? (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
|
||||
active || selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3 w-3 text-custom-text-200" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent px-2 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="Type to search..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-2 space-y-1 ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} overflow-y-scroll`}
|
||||
>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{multiple ? (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
|
||||
active || selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
</div>
|
||||
) : (
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
</div>
|
||||
) : (
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</div>
|
||||
{footerOption}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
// react-popper
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "../hooks/use-outside-click-detector";
|
||||
// headless ui
|
||||
import { Listbox } from "@headlessui/react";
|
||||
// icons
|
||||
@ -26,20 +29,36 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
optionsClassName = "",
|
||||
value,
|
||||
width = "auto",
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`relative flex-shrink-0 text-left ${className}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
@ -51,6 +70,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
className={`flex items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
@ -65,6 +85,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
} ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
@ -72,26 +93,28 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
</Listbox.Button>
|
||||
)}
|
||||
</>
|
||||
<Listbox.Options>
|
||||
<div
|
||||
className={`z-10 my-1 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="space-y-1 p-2">{children}</div>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
{isOpen && (
|
||||
<Listbox.Options static>
|
||||
<div
|
||||
className={`z-10 my-1 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="space-y-1 p-2">{children}</div>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ export interface IDropdownProps {
|
||||
optionsClassName?: string;
|
||||
width?: "auto" | string;
|
||||
placement?: Placement;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
|
24
packages/ui/src/hooks/use-dropdown-key-down.tsx
Normal file
24
packages/ui/src/hooks/use-dropdown-key-down.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.stopPropagation();
|
||||
if (!isOpen) {
|
||||
onOpen();
|
||||
}
|
||||
} else if (event.key === "Escape" && isOpen) {
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isOpen, onOpen, onClose]
|
||||
);
|
||||
|
||||
return handleKeyDown;
|
||||
};
|
19
packages/ui/src/hooks/use-outside-click-detector.tsx
Normal file
19
packages/ui/src/hooks/use-outside-click-detector.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default useOutsideClickDetector;
|
@ -4,10 +4,11 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||
import { Tab, Popover } from "@headlessui/react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import { useApplication, useWorkspace } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
@ -38,13 +39,14 @@ type Props = {
|
||||
control: Control<any>;
|
||||
onChange: (data: string) => void;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
const { label, value, control, onChange, disabled = false } = props;
|
||||
const { label, value, control, onChange, disabled = false, tabIndex } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
@ -128,27 +130,27 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
onChange(unsplashImages[0].urls.regular);
|
||||
}, [value, onChange, unsplashImages]);
|
||||
|
||||
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||
const openDropdown = () => setIsOpen(true);
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
|
||||
useOutsideClickDetector(ref, closeDropdown);
|
||||
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref}>
|
||||
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
||||
<Popover.Button
|
||||
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
onClick={openDropdown}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-sm">
|
||||
|
||||
{isOpen && (
|
||||
<Popover.Panel
|
||||
className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
static
|
||||
>
|
||||
<div
|
||||
ref={imagePickerRef}
|
||||
className="flex h-96 w-80 flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl md:h-[28rem] md:w-[36rem]"
|
||||
@ -349,7 +351,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
@ -59,6 +59,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
setActiveProject(val);
|
||||
}}
|
||||
buttonVariant="background-with-text"
|
||||
tabIndex={7}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -89,6 +90,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
inputSize="md"
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors?.name)}
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -106,6 +108,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -124,6 +127,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
tabIndex={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -140,6 +144,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="End date"
|
||||
minDate={minDate}
|
||||
tabIndex={4}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -149,10 +154,10 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={5}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={6}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core";
|
||||
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";
|
||||
// icons
|
||||
import { ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
@ -26,6 +28,7 @@ type Props = {
|
||||
placement?: Placement;
|
||||
projectId: string;
|
||||
value: string | null;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -104,9 +107,13 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -166,15 +173,26 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedCycle = value ? getCycleById(value) : null;
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -182,6 +200,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -197,6 +216,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -241,53 +261,55 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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 matches found</p>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Popover } from "@headlessui/react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { CalendarDays, X } from "lucide-react";
|
||||
// import "react-datepicker/dist/react-datepicker.css";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
@ -25,6 +28,7 @@ type Props = {
|
||||
placement?: Placement;
|
||||
value: Date | string | null;
|
||||
closeOnSelect?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -124,7 +128,11 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placement,
|
||||
value,
|
||||
closeOnSelect = true,
|
||||
tabIndex,
|
||||
} = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -143,8 +151,16 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
|
||||
const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== "";
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Popover className="h-full flex-shrink-0">
|
||||
<Popover ref={dropdownRef} tabIndex={tabIndex} className="h-full flex-shrink-0" onKeyDown={handleKeyDown}>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<Popover.Button as={React.Fragment}>
|
||||
@ -159,6 +175,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -220,22 +237,24 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
) : null}
|
||||
</button>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="fixed z-10">
|
||||
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) close();
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
calendarClassName="shadow-custom-shadow-rg rounded"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
{isOpen && (
|
||||
<Popover.Panel className="fixed z-10" static>
|
||||
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : null}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
if (closeOnSelect) close();
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
calendarClassName="shadow-custom-shadow-rg rounded"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -7,6 +7,8 @@ import { Check, ChevronDown, Search, Triangle } from "lucide-react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
// hooks
|
||||
import { useApplication, useEstimate } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@ -24,6 +26,7 @@ type Props = {
|
||||
placement?: Placement;
|
||||
projectId: string;
|
||||
value: number | null;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -102,9 +105,13 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -160,15 +167,26 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedEstimate = value !== null ? getEstimatePointValue(value) : null;
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -176,6 +194,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -191,6 +210,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -235,53 +255,55 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
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, 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 { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
|
||||
// icons
|
||||
@ -33,9 +35,13 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -93,12 +99,23 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId);
|
||||
}, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]);
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...comboboxProps}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
@ -107,6 +124,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -122,6 +140,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -172,53 +191,55 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
1
web/components/dropdowns/member/types.d.ts
vendored
1
web/components/dropdowns/member/types.d.ts
vendored
@ -11,6 +11,7 @@ export type MemberDropdownProps = {
|
||||
dropdownArrow?: boolean;
|
||||
placeholder?: string;
|
||||
placement?: Placement;
|
||||
tabIndex?: number;
|
||||
} & (
|
||||
| {
|
||||
multiple: false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useModule } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
// helpers
|
||||
@ -26,6 +28,7 @@ type Props = {
|
||||
placement?: Placement;
|
||||
projectId: string;
|
||||
value: string | null;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
@ -104,9 +107,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -166,15 +173,26 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedModule = value ? getModuleById(value) : null;
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -182,6 +200,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -197,6 +216,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -241,53 +261,55 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { Fragment, ReactNode, useState } from "react";
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// helpers
|
||||
@ -25,6 +28,7 @@ type Props = {
|
||||
onChange: (val: TIssuePriorities) => void;
|
||||
placement?: Placement;
|
||||
value: TIssuePriorities;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -210,9 +214,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
onChange,
|
||||
placement,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -269,15 +277,26 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
const filteredOptions =
|
||||
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -285,6 +304,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -300,6 +320,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -350,49 +371,51 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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}
|
||||
/>
|
||||
{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
|
||||
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.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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useState } from "react";
|
||||
import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
@ -24,6 +26,7 @@ type Props = {
|
||||
onChange: (val: string) => void;
|
||||
placement?: Placement;
|
||||
value: string | null;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -99,9 +102,13 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange,
|
||||
placement,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -146,15 +153,26 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedProject = value ? getProjectById(value) : null;
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -162,6 +180,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -177,6 +196,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -221,53 +241,55 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useProjectState } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
@ -26,6 +28,7 @@ type Props = {
|
||||
placement?: Placement;
|
||||
projectId: string;
|
||||
value: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -96,9 +99,13 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -144,15 +151,26 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedState = getStateById(value);
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full flex-shrink-0", {
|
||||
className,
|
||||
})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
{button ? (
|
||||
@ -160,6 +178,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
@ -175,6 +194,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{buttonVariant === "border-with-text" ? (
|
||||
<BorderButton
|
||||
@ -219,53 +239,55 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
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>
|
||||
))
|
||||
{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
|
||||
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 matches found</p>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
1
web/components/emoji-icon-picker/types.d.ts
vendored
1
web/components/emoji-icon-picker/types.d.ts
vendored
@ -11,4 +11,5 @@ export type Props = {
|
||||
) => void;
|
||||
onIconColorChange?: (data: any) => void;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import React, { Fragment, useRef, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Popover } from "@headlessui/react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
@ -12,20 +15,32 @@ type Props = {
|
||||
title?: string;
|
||||
placement?: Placement;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
const { children, title = "Dropdown", placement, disabled = false } = props;
|
||||
const { children, title = "Dropdown", placement, disabled = false, tabIndex } = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "auto",
|
||||
});
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Popover as="div">
|
||||
<Popover as="div" ref={dropdownRef} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
||||
{({ open }) => {
|
||||
if (open) {
|
||||
}
|
||||
@ -40,22 +55,15 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
appendIcon={
|
||||
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
||||
}
|
||||
onClick={openDropdown}
|
||||
>
|
||||
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel>
|
||||
{isOpen && (
|
||||
<Popover.Panel static>
|
||||
<div
|
||||
className="z-10 overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"
|
||||
ref={setPopperElement}
|
||||
@ -65,7 +73,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
<div className="flex max-h-[37.5rem] w-[18.75rem] flex-col overflow-hidden">{children}</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
@ -209,6 +209,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={19}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -238,6 +239,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(null);
|
||||
}}
|
||||
tabIndex={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -268,6 +270,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Issue Title"
|
||||
className="resize-none text-xl w-full"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -281,6 +284,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
tabIndex={3}
|
||||
>
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response"
|
||||
@ -309,6 +313,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||
tabIndex={4}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
@ -340,6 +345,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
mentionHighlights={mentionHighlights}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
// tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -358,6 +364,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={6}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -374,6 +381,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={7}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -394,6 +402,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
tabIndex={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -411,6 +420,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
tabIndex={9}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -429,6 +439,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
tabIndex={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -447,6 +458,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Due date"
|
||||
minDate={minDate ?? undefined}
|
||||
tabIndex={11}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -465,6 +477,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
value={value}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={12}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -484,6 +497,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={13}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -503,6 +517,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={14}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -532,6 +547,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
tabIndex={15}
|
||||
>
|
||||
{watch("parent_id") ? (
|
||||
<>
|
||||
@ -578,6 +594,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
<div
|
||||
className="flex cursor-default items-center gap-1.5"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") setCreateMore((prevData) => !prevData);
|
||||
}}
|
||||
tabIndex={16}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center justify-center">
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
@ -585,10 +605,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={17}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="sm" loading={isSubmitting}>
|
||||
<Button type="submit" variant="primary" size="sm" loading={isSubmitting} tabIndex={18}>
|
||||
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import React, { Fragment, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useLabel } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { IssueLabelsList } from "components/ui";
|
||||
// icons
|
||||
@ -18,10 +20,11 @@ type Props = {
|
||||
projectId: string;
|
||||
label?: JSX.Element;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const { setIsOpen, value, onChange, projectId, label, disabled = false } = props;
|
||||
const { setIsOpen, value, onChange, projectId, label, disabled = false, tabIndex } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -33,6 +36,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
@ -46,86 +52,122 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId) : null
|
||||
);
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsDropdownOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsDropdownOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
value={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
className="relative flex-shrink-0 h-full"
|
||||
multiple
|
||||
disabled={disabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{({ open }: any) => (
|
||||
<>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
|
||||
>
|
||||
{label ? (
|
||||
label
|
||||
) : value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 text-xs">
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => projectLabels?.find((l) => l.id === v)) ?? []}
|
||||
length={3}
|
||||
showLength
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<Tag className="h-3 w-3 flex-shrink-0" />
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
|
||||
onClick={openDropdown}
|
||||
>
|
||||
{label ? (
|
||||
label
|
||||
) : value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 text-xs">
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => projectLabels?.find((l) => l.id === v)) ?? []}
|
||||
length={3}
|
||||
showLength
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<Tag className="h-3 w-3 flex-shrink-0" />
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
{isDropdownOpen && (
|
||||
<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}
|
||||
>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<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
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{projectLabels && filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((label) => {
|
||||
const children = projectLabels?.filter((l) => l.parent === label.id);
|
||||
<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
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||
{projectLabels && filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((label) => {
|
||||
const children = projectLabels?.filter((l) => l.parent === label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-200">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||
<Component className="h-3 w-3" /> {label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
key={child.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={label.id}
|
||||
value={child.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
@ -133,10 +175,10 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
backgroundColor: child?.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
<span>{child.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
@ -144,65 +186,28 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-200">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||
<Component className="h-3 w-3" /> {label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Combobox.Option
|
||||
key={child.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={child.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: child?.color,
|
||||
}}
|
||||
/>
|
||||
<span>{child.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<Check className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 w-full select-none rounded px-1 py-2 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Create new label</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 w-full select-none rounded px-1 py-2 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Create new label</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
|
@ -93,6 +93,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
setActiveProject(val);
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -124,6 +125,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Module Title"
|
||||
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -141,6 +143,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
placeholder="Description..."
|
||||
className="h-24 w-full resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -157,6 +160,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
tabIndex={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -172,11 +176,12 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Target date"
|
||||
minDate={minDate ?? undefined}
|
||||
tabIndex={4}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<ModuleStatusSelect control={control} error={errors.status} />
|
||||
<ModuleStatusSelect control={control} error={errors.status} tabIndex={5} />
|
||||
<Controller
|
||||
control={control}
|
||||
name="lead"
|
||||
@ -189,6 +194,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
multiple={false}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Lead"
|
||||
tabIndex={6}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -206,6 +212,7 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
buttonVariant={value && value.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
placeholder="Members"
|
||||
tabIndex={7}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -214,10 +221,10 @@ export const ModuleForm: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200 pt-5">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={8}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={9}>
|
||||
{status ? (isSubmitting ? "Updating" : "Update module") : isSubmitting ? "Creating" : "Create module"}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -12,9 +12,10 @@ import { MODULE_STATUS } from "constants/module";
|
||||
type Props = {
|
||||
control: Control<IModule, any>;
|
||||
error?: FieldError;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const ModuleStatusSelect: React.FC<Props> = ({ control, error }) => (
|
||||
export const ModuleStatusSelect: React.FC<Props> = ({ control, error, tabIndex }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
@ -35,6 +36,7 @@ export const ModuleStatusSelect: React.FC<Props> = ({ control, error }) => (
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
tabIndex={tabIndex}
|
||||
noChevron
|
||||
>
|
||||
{MODULE_STATUS.map((status) => (
|
||||
|
@ -59,6 +59,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-lg"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -72,7 +73,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
|
||||
{PAGE_ACCESS_SPECIFIERS.map((access) => (
|
||||
{PAGE_ACCESS_SPECIFIERS.map((access, index) => (
|
||||
<Tooltip key={access.key} tooltipContent={access.label}>
|
||||
<button
|
||||
type="button"
|
||||
@ -80,6 +81,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
className={`grid aspect-square place-items-center rounded-sm p-1 hover:bg-custom-background-90 ${
|
||||
value === access.key ? "bg-custom-background-90" : ""
|
||||
}`}
|
||||
tabIndex={2 + index}
|
||||
>
|
||||
<access.icon
|
||||
className={`h-3.5 w-3.5 ${
|
||||
@ -98,10 +100,10 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
|
||||
{data ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -226,7 +226,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
<div className="absolute right-2 top-2 p-2">
|
||||
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose}>
|
||||
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={8}>
|
||||
<X className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
@ -238,6 +238,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
}}
|
||||
control={control}
|
||||
value={watch("cover_image")}
|
||||
tabIndex={9}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-[22px] left-3">
|
||||
@ -253,6 +254,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
tabIndex={10}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -278,11 +280,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
tabIndex={1}
|
||||
onChange={handleNameChange(onChange)}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Project Title"
|
||||
className="w-full focus:border-blue-400"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -313,11 +315,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
name="identifier"
|
||||
type="text"
|
||||
value={value}
|
||||
tabIndex={2}
|
||||
onChange={handleIdentifierChange(onChange)}
|
||||
hasError={Boolean(errors.identifier)}
|
||||
placeholder="Identifier"
|
||||
className="w-full text-xs focus:border-blue-400 uppercase"
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -332,11 +334,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
id="description"
|
||||
name="description"
|
||||
value={value}
|
||||
tabIndex={3}
|
||||
placeholder="Description..."
|
||||
onChange={onChange}
|
||||
className="!h-24 text-sm focus:border-blue-400"
|
||||
hasError={Boolean(errors?.description)}
|
||||
tabIndex={3}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -366,6 +368,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
}
|
||||
placement="bottom-start"
|
||||
noChevron
|
||||
tabIndex={4}
|
||||
>
|
||||
{NETWORK_CHOICES.map((network) => (
|
||||
<CustomSelect.Option
|
||||
@ -392,6 +395,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
placeholder="Lead"
|
||||
multiple={false}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={5}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -130,6 +130,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl focus:border-blue-400"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -147,6 +148,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -156,7 +158,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
control={control}
|
||||
name="query_data"
|
||||
render={({ field: { onChange, value: filters } }) => (
|
||||
<FiltersDropdown title="Filters">
|
||||
<FiltersDropdown title="Filters" tabIndex={3}>
|
||||
<FilterSelection
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={(key, value) => {
|
||||
@ -199,10 +201,10 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit">
|
||||
<Button variant="primary" size="sm" type="submit" tabIndex={5}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating View..."
|
||||
|
24
web/hooks/use-dropdown-key-down.tsx
Normal file
24
web/hooks/use-dropdown-key-down.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.stopPropagation();
|
||||
if (!isOpen) {
|
||||
onOpen();
|
||||
}
|
||||
} else if (event.key === "Escape" && isOpen) {
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isOpen, onOpen, onClose]
|
||||
);
|
||||
|
||||
return handleKeyDown;
|
||||
};
|
@ -1,95 +1,214 @@
|
||||
import { observable, runInAction } from "mobx";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import omit from "lodash/omit";
|
||||
import isToday from "date-fns/isToday";
|
||||
import isThisWeek from "date-fns/isThisWeek";
|
||||
import isYesterday from "date-fns/isYesterday";
|
||||
|
||||
import { IPage } from "@plane/types";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
import { is } from "date-fns/locale";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IPage, IRecentPages } from "@plane/types";
|
||||
// store
|
||||
import { RootStore } from "./root.store";
|
||||
|
||||
export interface IPageStore {
|
||||
access: number;
|
||||
archived_at: string | null;
|
||||
color: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
description: string;
|
||||
description_html: string;
|
||||
description_stripped: string | null;
|
||||
id: string;
|
||||
is_favorite: boolean;
|
||||
is_locked: boolean;
|
||||
labels: string[];
|
||||
name: string;
|
||||
owned_by: string;
|
||||
project: string;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
pages: Record<string, IPage>;
|
||||
archivedPages: Record<string, IPage>;
|
||||
// project computed
|
||||
projectPageIds: string[] | null;
|
||||
favoriteProjectPageIds: string[] | null;
|
||||
privateProjectPageIds: string[] | null;
|
||||
publicProjectPageIds: string[] | null;
|
||||
archivedProjectPageIds: string[] | null;
|
||||
recentProjectPages: IRecentPages | null;
|
||||
// fetch page information actions
|
||||
getUnArchivedPageById: (pageId: string) => IPage | null;
|
||||
getArchivedPageById: (pageId: string) => IPage | null;
|
||||
// fetch actions
|
||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
||||
// favorites actions
|
||||
addToFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
removeFromFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// crud
|
||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||
updatePage: (workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// access control actions
|
||||
makePublic: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
makePrivate: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// archive actions
|
||||
archivePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
restorePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class PageStore {
|
||||
access: number;
|
||||
archived_at: string | null;
|
||||
color: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
description: string;
|
||||
description_html: string;
|
||||
description_stripped: string | null;
|
||||
id: string;
|
||||
is_favorite: boolean;
|
||||
is_locked: boolean;
|
||||
labels: string[];
|
||||
name: string;
|
||||
owned_by: string;
|
||||
project: string;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
|
||||
export class PageStore implements IPageStore {
|
||||
pages: Record<string, IPage> = {};
|
||||
archivedPages: Record<string, IPage> = {};
|
||||
// services
|
||||
pageService;
|
||||
// stores
|
||||
rootStore;
|
||||
|
||||
constructor(page: IPage) {
|
||||
observable(this, {
|
||||
name: observable.ref,
|
||||
description_html: observable.ref,
|
||||
is_favorite: observable.ref,
|
||||
is_locked: observable.ref,
|
||||
constructor(rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
pages: observable,
|
||||
archivedPages: observable,
|
||||
// computed
|
||||
projectPageIds: computed,
|
||||
favoriteProjectPageIds: computed,
|
||||
publicProjectPageIds: computed,
|
||||
privateProjectPageIds: computed,
|
||||
archivedProjectPageIds: computed,
|
||||
recentProjectPages: computed,
|
||||
// computed actions
|
||||
getUnArchivedPageById: action,
|
||||
getArchivedPageById: action,
|
||||
// fetch actions
|
||||
fetchProjectPages: action,
|
||||
fetchArchivedProjectPages: action,
|
||||
// favorites actions
|
||||
addToFavorites: action,
|
||||
removeFromFavorites: action,
|
||||
// crud
|
||||
createPage: action,
|
||||
updatePage: action,
|
||||
deletePage: action,
|
||||
// access control actions
|
||||
makePublic: action,
|
||||
makePrivate: action,
|
||||
// archive actions
|
||||
archivePage: action,
|
||||
restorePage: action,
|
||||
});
|
||||
this.created_by = page?.created_by || "";
|
||||
this.created_at = page?.created_at || new Date();
|
||||
this.color = page?.color || "";
|
||||
this.archived_at = page?.archived_at || null;
|
||||
this.name = page?.name || "";
|
||||
this.description = page?.description || "";
|
||||
this.description_stripped = page?.description_stripped || "";
|
||||
this.description_html = page?.description_html || "";
|
||||
this.access = page?.access || 0;
|
||||
this.workspace = page?.workspace || "";
|
||||
this.updated_by = page?.updated_by || "";
|
||||
this.updated_at = page?.updated_at || new Date();
|
||||
this.project = page?.project || "";
|
||||
this.owned_by = page?.owned_by || "";
|
||||
this.labels = page?.labels || [];
|
||||
this.is_locked = page?.is_locked || false;
|
||||
this.id = page?.id || "";
|
||||
this.is_favorite = page?.is_favorite || false;
|
||||
|
||||
// stores
|
||||
this.rootStore = rootStore;
|
||||
// services
|
||||
this.pageService = new PageService();
|
||||
}
|
||||
|
||||
updateName = async (name: string) => {
|
||||
this.name = name;
|
||||
await this.pageService.patchPage(this.workspace, this.project, this.id, { name });
|
||||
};
|
||||
/**
|
||||
* retrieves all pages for a projectId that is available in the url.
|
||||
*/
|
||||
get projectPageIds() {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId) return null;
|
||||
const projectPageIds = Object.keys(this.pages).filter((pageId) => this.pages?.[pageId]?.project === projectId);
|
||||
return projectPageIds ?? null;
|
||||
}
|
||||
|
||||
updateDescription = async (description: string) => {
|
||||
this.description = description;
|
||||
await this.pageService.patchPage(this.workspace, this.project, this.id, { description });
|
||||
};
|
||||
/**
|
||||
* retrieves all favorite pages for a projectId that is available in the url.
|
||||
*/
|
||||
get favoriteProjectPageIds() {
|
||||
if (!this.projectPageIds) return null;
|
||||
const favoritePagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.is_favorite);
|
||||
return favoritePagesIds ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all private pages for a projectId that is available in the url.
|
||||
*/
|
||||
get privateProjectPageIds() {
|
||||
if (!this.projectPageIds) return null;
|
||||
const privatePagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.access === 1);
|
||||
return privatePagesIds ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all shared pages which are public to everyone in the project for a projectId that is available in the url.
|
||||
*/
|
||||
get publicProjectPageIds() {
|
||||
if (!this.projectPageIds) return null;
|
||||
const publicPagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.access === 0);
|
||||
return publicPagesIds ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all recent pages for a projectId that is available in the url.
|
||||
* In format where today, yesterday, this_week, older are keys.
|
||||
*/
|
||||
get recentProjectPages() {
|
||||
if (!this.projectPageIds) return null;
|
||||
const data: IRecentPages = { today: [], yesterday: [], this_week: [], older: [] };
|
||||
data.today = this.projectPageIds.filter((p) => isToday(new Date(this.pages?.[p]?.created_at))) || [];
|
||||
data.yesterday = this.projectPageIds.filter((p) => isYesterday(new Date(this.pages?.[p]?.created_at))) || [];
|
||||
data.this_week =
|
||||
this.projectPageIds.filter((p) => {
|
||||
const pageCreatedAt = this.pages?.[p]?.created_at;
|
||||
return (
|
||||
isThisWeek(new Date(pageCreatedAt)) &&
|
||||
!isToday(new Date(pageCreatedAt)) &&
|
||||
!isYesterday(new Date(pageCreatedAt))
|
||||
);
|
||||
}) || [];
|
||||
data.older =
|
||||
this.projectPageIds.filter((p) => {
|
||||
const pageCreatedAt = this.pages?.[p]?.created_at;
|
||||
return !isThisWeek(new Date(pageCreatedAt)) && !isYesterday(new Date(pageCreatedAt));
|
||||
}) || [];
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all archived pages for a projectId that is available in the url.
|
||||
*/
|
||||
get archivedProjectPageIds() {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId) return null;
|
||||
const archivedProjectPageIds = Object.keys(this.archivedPages).filter(
|
||||
(pageId) => this.archivedPages?.[pageId]?.project === projectId
|
||||
);
|
||||
return archivedProjectPageIds ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves a page from pages by id.
|
||||
* @param pageId
|
||||
* @returns IPage | null
|
||||
*/
|
||||
getUnArchivedPageById = (pageId: string) => this.pages?.[pageId] ?? null;
|
||||
|
||||
/**
|
||||
* retrieves a page from archived pages by id.
|
||||
* @param pageId
|
||||
* @returns IPage | null
|
||||
*/
|
||||
getArchivedPageById = (pageId: string) => this.archivedPages?.[pageId] ?? null;
|
||||
|
||||
/**
|
||||
* fetches all pages for a project.
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @returns Promise<IPage[]>
|
||||
*/
|
||||
fetchProjectPages = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((page) => {
|
||||
set(this.pages, [page.id], page);
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* fetches all archived pages for a project.
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @returns Promise<IPage[]>
|
||||
*/
|
||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((page) => {
|
||||
set(this.archivedPages, [page.id], page);
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* Add Page to users favorites list
|
||||
@ -100,11 +219,13 @@ export class PageStore {
|
||||
addToFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.is_favorite = true;
|
||||
set(this.pages, [pageId, "is_favorite"], true);
|
||||
});
|
||||
await this.pageService.addPageToFavorites(workspaceSlug, projectId, pageId);
|
||||
} catch (error) {
|
||||
this.is_favorite = false;
|
||||
runInAction(() => {
|
||||
set(this.pages, [pageId, "is_favorite"], false);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@ -117,13 +238,62 @@ export class PageStore {
|
||||
*/
|
||||
removeFromFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
this.is_favorite = false;
|
||||
runInAction(() => {
|
||||
set(this.pages, [pageId, "is_favorite"], false);
|
||||
});
|
||||
await this.pageService.removePageFromFavorites(workspaceSlug, projectId, pageId);
|
||||
} catch (error) {
|
||||
this.is_favorite = true;
|
||||
runInAction(() => {
|
||||
set(this.pages, [pageId, "is_favorite"], true);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Creates a new page using the api and updated the local state in store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
*/
|
||||
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) =>
|
||||
await this.pageService.createPage(workspaceSlug, projectId, data).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.pages, [response.id], response);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* updates the page using the api and updates the local state in store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
updatePage = async (workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>) =>
|
||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, data).then((response) => {
|
||||
const originalPage = this.getUnArchivedPageById(pageId);
|
||||
runInAction(() => {
|
||||
set(this.pages, [pageId], { ...originalPage, ...data });
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* delete a page using the api and updates the local state in store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
* @returns
|
||||
*/
|
||||
deletePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
||||
await this.pageService.deletePage(workspaceSlug, projectId, pageId).then((response) => {
|
||||
runInAction(() => {
|
||||
omit(this.archivedPages, [pageId]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* make a page public
|
||||
@ -135,12 +305,12 @@ export class PageStore {
|
||||
makePublic = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.access = 0;
|
||||
set(this.pages, [pageId, "access"], 0);
|
||||
});
|
||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 0 });
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.access = 1;
|
||||
set(this.pages, [pageId, "access"], 1);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@ -156,14 +326,43 @@ export class PageStore {
|
||||
makePrivate = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.access = 1;
|
||||
set(this.pages, [pageId, "access"], 1);
|
||||
});
|
||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 1 });
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.access = 0;
|
||||
set(this.pages, [pageId, "access"], 0);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a page archived
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
||||
await this.pageService.archivePage(workspaceSlug, projectId, pageId).then(() => {
|
||||
runInAction(() => {
|
||||
set(this.archivedPages, [pageId], this.pages[pageId]);
|
||||
set(this.archivedPages, [pageId, "archived_at"], renderFormattedPayloadDate(new Date()));
|
||||
omit(this.pages, [pageId]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Restore a page from archived pages to pages
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
||||
await this.pageService.restorePage(workspaceSlug, projectId, pageId).then(() => {
|
||||
runInAction(() => {
|
||||
set(this.pages, [pageId], this.archivedPages[pageId]);
|
||||
omit(this.archivedPages, [pageId]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
fetchProjectPages = async (workspaceSlug: string, projectId: string) => {
|
||||
const response = await this.pageService.getProjectPages(workspaceSlug, projectId);
|
||||
runInAction(() => {
|
||||
this.projectPages[projectId] = response?.map((page) => new PageStore(page));
|
||||
this.projectPages[projectId] = response?.map((page) => new PageStore(page as any));
|
||||
});
|
||||
};
|
||||
|
||||
@ -59,7 +59,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
this.projectArchivedPages[projectId] = response?.map((page) => new PageStore(page));
|
||||
this.projectArchivedPages[projectId] = response?.map((page) => new PageStore(page as any));
|
||||
});
|
||||
return response;
|
||||
});
|
||||
@ -73,7 +73,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) => {
|
||||
const response = await this.pageService.createPage(workspaceSlug, projectId, data);
|
||||
runInAction(() => {
|
||||
this.projectPages[projectId] = [...this.projectPages[projectId], new PageStore(response)];
|
||||
this.projectPages[projectId] = [...this.projectPages[projectId], new PageStore(response as any)];
|
||||
});
|
||||
return response;
|
||||
};
|
||||
@ -91,7 +91,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
this.projectPages = set(
|
||||
this.projectPages,
|
||||
[projectId],
|
||||
this.projectPages[projectId].filter((page) => page.id !== pageId)
|
||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
});
|
||||
return response;
|
||||
@ -109,7 +109,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
set(
|
||||
this.projectPages,
|
||||
[projectId],
|
||||
this.projectPages[projectId].filter((page) => page.id !== pageId)
|
||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
this.projectArchivedPages = set(this.projectArchivedPages, [projectId], this.projectArchivedPages[projectId]);
|
||||
});
|
||||
@ -128,7 +128,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
set(
|
||||
this.projectArchivedPages,
|
||||
[projectId],
|
||||
this.projectArchivedPages[projectId].filter((page) => page.id !== pageId)
|
||||
this.projectArchivedPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
set(this.projectPages, [projectId], [...this.projectPages[projectId]]);
|
||||
});
|
||||
|
@ -32,7 +32,7 @@ export class RootStore {
|
||||
module: IModuleStore;
|
||||
projectView: IProjectViewStore;
|
||||
globalView: IGlobalViewStore;
|
||||
// page: IPageStore;
|
||||
page: IPageStore;
|
||||
issue: IIssueRootStore;
|
||||
state: IStateStore;
|
||||
estimate: IEstimateStore;
|
||||
@ -57,5 +57,6 @@ export class RootStore {
|
||||
this.estimate = new EstimateStore(this);
|
||||
this.mention = new MentionStore(this);
|
||||
this.projectPages = new ProjectPageStore();
|
||||
this.page = new PageStore(this);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user