feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)
This commit is contained in:
committed by
GitHub
parent
d6b1ad1ccd
commit
40de54b010
+190
@@ -0,0 +1,190 @@
|
||||
import clsx from "clsx"
|
||||
import React, { ComponentPropsWithRef, forwardRef } from "react"
|
||||
import {
|
||||
ContainerProps,
|
||||
GroupBase,
|
||||
IndicatorsContainerProps,
|
||||
ValueContainerProps,
|
||||
} from "react-select"
|
||||
import InputError from "../../../../atoms/input-error"
|
||||
|
||||
type AdjacentContainerProps = {
|
||||
label?: string
|
||||
htmlFor?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
name?: string
|
||||
errors?: Record<string, unknown>
|
||||
children?: React.ReactNode
|
||||
} & ComponentPropsWithRef<"div">
|
||||
|
||||
export const AdjacentContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
AdjacentContainerProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
required,
|
||||
errors,
|
||||
name,
|
||||
children,
|
||||
}: AdjacentContainerProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-xsmall w-full" ref={ref}>
|
||||
{label && (
|
||||
<label
|
||||
className="inter-small-semibold text-grey-50"
|
||||
id={`${name}_label`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-rose-50">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
{name && errors ? (
|
||||
<InputError errors={errors} name={name} className="-mt-0.5" />
|
||||
) : helperText ? (
|
||||
<p className="inter-small-regular text-grey-50">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const SelectContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
selectProps: { isDisabled, isRtl },
|
||||
hasValue,
|
||||
cx,
|
||||
className,
|
||||
children,
|
||||
}: ContainerProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"--is-disabled": isDisabled,
|
||||
"--is-rtl": isRtl,
|
||||
"--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"relative pointer-events-auto",
|
||||
{ "text-grey-40": isDisabled },
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValueContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: ValueContainerProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
cx,
|
||||
innerProps,
|
||||
isMulti,
|
||||
hasValue,
|
||||
selectProps: { value, inputValue, label, selectedPlaceholder },
|
||||
} = props
|
||||
|
||||
if (isMulti && Array.isArray(value)) {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"value-container": true,
|
||||
"value-container--is-multi": isMulti,
|
||||
"value-container--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"group flex items-center flex-wrap relative scrolling-touch overflow-hidden flex-1",
|
||||
{
|
||||
"gap-2xsmall": isMulti,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{value?.length > 0 && (
|
||||
<div className="h-7 bg-grey-20 text-grey-50 px-small inter-small-semibold flex items-center rounded-rounded gap-x-2xsmall cursor-default">
|
||||
<span>{value.length}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative grow">
|
||||
{children}
|
||||
{value?.length > 0 && inputValue === "" && (
|
||||
<span className="absolute top-1/2 -translate-y-1/2 inter-base-regular text-grey-50">
|
||||
{selectedPlaceholder || label || "Selected"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"value-container": true,
|
||||
"value-container--is-multi": isMulti,
|
||||
"value-container--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"flex items-center flex-wrap relative scrolling-touch overflow-hidden flex-1",
|
||||
{
|
||||
"gap-2xsmall": isMulti,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const IndicatorsContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
innerProps,
|
||||
children,
|
||||
}: IndicatorsContainerProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"indicators-container": true,
|
||||
},
|
||||
clsx("text-grey-50 flex items-center gap-x-small px-small", className)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import clsx from "clsx"
|
||||
import {
|
||||
ClearIndicatorProps,
|
||||
ControlProps,
|
||||
DropdownIndicatorProps,
|
||||
GroupBase,
|
||||
LoadingIndicatorProps,
|
||||
} from "react-select"
|
||||
import Spinner from "../../../../atoms/spinner"
|
||||
import ChevronDownIcon from "../../../../fundamentals/icons/chevron-down"
|
||||
import XCircleIcon from "../../../../fundamentals/icons/x-circle-icon"
|
||||
|
||||
const Control = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
innerRef,
|
||||
innerProps,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
menuIsOpen,
|
||||
selectProps: { size, customStyles, name },
|
||||
}: ControlProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
{...innerProps}
|
||||
id={name}
|
||||
className={cx(
|
||||
{
|
||||
control: true,
|
||||
"control--is-disabled": isDisabled,
|
||||
"control--is-focused": isFocused,
|
||||
"control--menu-is-open": menuIsOpen,
|
||||
},
|
||||
clsx(
|
||||
"flex p-0 overflow-hidden rounded-rounded border border-gray-20 bg-grey-5 focus-within:shadow-cta transition-colors focus-within:border-violet-60 box-border pl-small",
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
},
|
||||
className,
|
||||
customStyles?.control
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center flex-1",
|
||||
customStyles?.inner_control
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Control
|
||||
|
||||
export const DropdownIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
cx,
|
||||
children,
|
||||
className,
|
||||
selectProps: { menuIsOpen },
|
||||
}: DropdownIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"dropdown-indicator": true,
|
||||
},
|
||||
clsx(
|
||||
"transition-all",
|
||||
{
|
||||
"rotate-180": menuIsOpen,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children || <ChevronDownIcon size={16} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
className,
|
||||
cx,
|
||||
}: LoadingIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"loading-indicator": true,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Spinner size="small" variant="secondary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClearIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
}: ClearIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
role="button"
|
||||
aria-label="Clear selected options"
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"clear-indicator": true,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children || <XCircleIcon size={16} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
AdjacentContainer,
|
||||
IndicatorsContainer,
|
||||
SelectContainer,
|
||||
ValueContainer,
|
||||
} from "./containers"
|
||||
import Control, {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
LoadingIndicator,
|
||||
} from "./control"
|
||||
import Input from "./input"
|
||||
import Menu, { LoadingMessage, MenuList, Option } from "./menu"
|
||||
import Placeholder from "./placeholder"
|
||||
import SingleValue from "./single-value"
|
||||
|
||||
const Components = {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
LoadingIndicator,
|
||||
SelectContainer,
|
||||
Control,
|
||||
Input,
|
||||
Placeholder,
|
||||
Menu,
|
||||
MenuList,
|
||||
Option,
|
||||
SingleValue,
|
||||
MultiValue: () => null,
|
||||
MultiValueContainer: () => null,
|
||||
MultiValueRemove: () => null,
|
||||
ValueContainer,
|
||||
IndicatorsContainer,
|
||||
LoadingMessage,
|
||||
IndicatorSeparator: null,
|
||||
}
|
||||
|
||||
export { AdjacentContainer }
|
||||
export default Components
|
||||
@@ -0,0 +1,36 @@
|
||||
import clsx from "clsx"
|
||||
import { GroupBase, InputProps } from "react-select"
|
||||
import SelectPrimitives from "./select-primitives"
|
||||
|
||||
const Input = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: InputProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const { className, cx, value, inputClassName } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
data-value={value || ""}
|
||||
className={cx({ "input-container": true }, className)}
|
||||
>
|
||||
<SelectPrimitives.Input
|
||||
{...props}
|
||||
className={cx(
|
||||
{
|
||||
input: true,
|
||||
"input--is-disabled": props.isDisabled ? true : false,
|
||||
},
|
||||
clsx(
|
||||
"inter-base-regular text-grey-90 caret-violet-60",
|
||||
inputClassName
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,343 @@
|
||||
import clsx from "clsx"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import {
|
||||
ActionMeta,
|
||||
CX,
|
||||
GroupBase,
|
||||
MenuListProps,
|
||||
MenuProps,
|
||||
NoticeProps,
|
||||
OnChangeValue,
|
||||
OptionProps,
|
||||
OptionsOrGroups,
|
||||
PropsValue,
|
||||
} from "react-select"
|
||||
import Button from "../../../../fundamentals/button"
|
||||
import CheckIcon from "../../../../fundamentals/icons/check-icon"
|
||||
import ListArrowIcon from "../../../../fundamentals/icons/list-arrow-icon"
|
||||
import { hasPrefix, hasSuffix, optionIsDisabled } from "../utils"
|
||||
import SelectPrimitives from "./select-primitives"
|
||||
|
||||
const Menu = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
innerProps,
|
||||
innerRef,
|
||||
placement,
|
||||
selectProps: { onMenuClose, menuIsOpen, customStyles, styles },
|
||||
}: MenuProps<Option, IsMulti, Group>) => {
|
||||
useEffect(() => {
|
||||
const closeOnResize = () => {
|
||||
if (menuIsOpen) {
|
||||
onMenuClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", closeOnResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", closeOnResize)
|
||||
}
|
||||
}, [menuIsOpen, onMenuClose])
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
ref={innerRef}
|
||||
style={{ ...styles?.menu }}
|
||||
className={cx(
|
||||
{ menu: true },
|
||||
clsx(
|
||||
"absolute w-full overflow-hidden border-border z-[60] bg-grey-0 rounded-rounded border border-grey-20 shadow-dropdown mb-base",
|
||||
{
|
||||
"top-[calc(100%+8px)]": placement === "bottom",
|
||||
"bottom-full": placement === "top",
|
||||
},
|
||||
className,
|
||||
customStyles?.menu
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
|
||||
type SelectAllOptionProps<
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
> = {
|
||||
cx: CX
|
||||
onChange: (
|
||||
newValue: OnChangeValue<Option, IsMulti>,
|
||||
actionMeta: ActionMeta<Option>
|
||||
) => void
|
||||
options: OptionsOrGroups<Option, Group>
|
||||
value: PropsValue<Option>
|
||||
}
|
||||
|
||||
const SelectAllOption = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
cx,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: SelectAllOptionProps<Option, IsMulti, Group>) => {
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
if (Array.isArray(value)) {
|
||||
const selectableOptions = options.filter((o) => !optionIsDisabled(o))
|
||||
return value.length === selectableOptions.length
|
||||
}
|
||||
|
||||
return false
|
||||
}, [options, value])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (isAllSelected) {
|
||||
onChange([] as unknown as OnChangeValue<Option, IsMulti>, {
|
||||
action: "deselect-option",
|
||||
option: [] as unknown as Option,
|
||||
})
|
||||
} else {
|
||||
const selectableOptions = options.filter((o) => !optionIsDisabled(o))
|
||||
|
||||
onChange(selectableOptions as unknown as OnChangeValue<Option, IsMulti>, {
|
||||
action: "select-option",
|
||||
option: selectableOptions as unknown as Option,
|
||||
})
|
||||
}
|
||||
}, [isAllSelected, onChange, options])
|
||||
|
||||
useEffect(() => {
|
||||
setIsFocused(
|
||||
document.activeElement !== null && document.activeElement === ref.current
|
||||
)
|
||||
|
||||
return () => {
|
||||
setIsFocused(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className={cx(
|
||||
{
|
||||
option: true,
|
||||
"option--is-focused": isFocused,
|
||||
},
|
||||
clsx("mx-base mb-2xsmall h-xlarge")
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<ListArrowIcon size={16} />
|
||||
<span className="inter-small-semibold">
|
||||
{!isAllSelected ? "Select All" : "Deselect All"}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuList = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: MenuListProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
cx,
|
||||
isMulti,
|
||||
selectProps: { selectAll, value, onChange },
|
||||
options,
|
||||
} = props
|
||||
return (
|
||||
<SelectPrimitives.MenuList
|
||||
{...props}
|
||||
className={cx(
|
||||
{
|
||||
"menu-list": true,
|
||||
"menu-list--is-multi": isMulti,
|
||||
},
|
||||
clsx("overflow-y-auto flex flex-col py-xsmall no-scrollbar", className)
|
||||
)}
|
||||
>
|
||||
{isMulti && selectAll && (
|
||||
<SelectAllOption
|
||||
cx={cx}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</SelectPrimitives.MenuList>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingMessage = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
cx,
|
||||
className,
|
||||
selectProps: { size },
|
||||
}: NoticeProps<Option, IsMulti, Group>) => {
|
||||
const Skeleton = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full flex items-center px-base transition-colors hover:bg-grey-5",
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="bg-grey-10 animate-pulse w-1/4 h-xsmall rounded-rounded" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"menu-notice": true,
|
||||
"menu-notice--loading": true,
|
||||
},
|
||||
clsx("flex flex-col", className)
|
||||
)}
|
||||
>
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Option = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: OptionProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
children,
|
||||
cx,
|
||||
className,
|
||||
innerProps,
|
||||
innerRef,
|
||||
selectProps: { hideSelectedOptions, isMulti, size, truncateOption },
|
||||
} = props
|
||||
|
||||
const prefix = hasPrefix(props.data) ? props.data.prefix : null
|
||||
const suffix = hasSuffix(props.data) ? props.data.suffix : null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
className={cx(
|
||||
{
|
||||
option: true,
|
||||
"option--is-selected": isSelected,
|
||||
"option--is-disabled": isDisabled,
|
||||
"option--is-focused": isFocused,
|
||||
},
|
||||
clsx(
|
||||
"flex items-center justify-between py-xsmall px-base transition-colors hover:bg-grey-5",
|
||||
{
|
||||
"text-grey-30 select-none cursor-not-allowed": isDisabled,
|
||||
"bg-grey-10": isFocused && !isDisabled,
|
||||
hidden: hideSelectedOptions && isSelected,
|
||||
},
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
ref={innerRef}
|
||||
data-diabled={isDisabled ? true : undefined}
|
||||
aria-disabled={isDisabled ? true : undefined}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
{...innerProps}
|
||||
>
|
||||
<div className="flex items-center gap-x-small flex-1">
|
||||
{isMulti && (
|
||||
<CheckboxAdornment isSelected={isSelected} isDisabled={isDisabled} />
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-between gap-x-xsmall inter-base-regular flex-1",
|
||||
{
|
||||
truncate: !!truncateOption,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{prefix && <span className="inter-base-semibold">{prefix}</span>}
|
||||
<span className="w-full">{children}</span>
|
||||
|
||||
{suffix && (
|
||||
<span className="inter-base-regular justify-self-end text-grey-50">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isMulti && (
|
||||
<div className="w-5 ml-xsmall">
|
||||
{isSelected && <CheckIcon size={16} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckboxAdornment = ({
|
||||
isSelected,
|
||||
isDisabled,
|
||||
}: Pick<OptionProps, "isSelected" | "isDisabled">) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`w-base h-base flex justify-center text-grey-0 border-grey-30 border rounded-base transition-colors`,
|
||||
{
|
||||
"bg-violet-60 border-violet-60": isSelected,
|
||||
"bg-grey-5": isDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="self-center">
|
||||
{isSelected && <CheckIcon size={10} />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import {
|
||||
GroupBase,
|
||||
MultiValueGenericProps,
|
||||
MultiValueProps,
|
||||
MultiValueRemoveProps
|
||||
} from "react-select";
|
||||
import CrossIcon from "../../../../fundamentals/icons/cross-icon";
|
||||
import { optionIsFixed } from "../utils";
|
||||
|
||||
const MultiValue = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
children,
|
||||
className,
|
||||
cx,
|
||||
innerProps,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
removeProps,
|
||||
components,
|
||||
selectProps,
|
||||
data,
|
||||
}: MultiValueProps<Option, IsMulti, Group>) => {
|
||||
const { Container, Label, Remove } = components
|
||||
|
||||
return (
|
||||
<Container
|
||||
data={data}
|
||||
innerProps={{
|
||||
className: cx({
|
||||
"multi-value": true,
|
||||
"multi-value--is-disabled": isDisabled,
|
||||
}, clsx({
|
||||
"bg-grey-70 text-grey-0": isFocused
|
||||
})),
|
||||
...innerProps,
|
||||
}}
|
||||
selectProps={selectProps}
|
||||
>
|
||||
<Label
|
||||
data={data}
|
||||
innerProps={{
|
||||
className: cx(
|
||||
{
|
||||
"multi-value__label": true,
|
||||
},
|
||||
className
|
||||
),
|
||||
}}
|
||||
selectProps={selectProps}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
<Remove
|
||||
data={data}
|
||||
selectProps={selectProps}
|
||||
innerProps={{
|
||||
className: cx(
|
||||
{
|
||||
"multi-value__remove": true,
|
||||
},
|
||||
className
|
||||
),
|
||||
"aria-label": `Remove ${children || "option"}`,
|
||||
...removeProps,
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiValue
|
||||
|
||||
export const MultiValueContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
children,
|
||||
innerProps: { className },
|
||||
}: MultiValueGenericProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<span className={clsx("flex items-center gap-x-2xsmall py-2xsmall pl-small pr-2.5 rounded-rounded transition-colors bg-grey-20 text-grey-50 inter-small-semibold", className)}>{children}</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultiValueRemove = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = boolean,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: MultiValueRemoveProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const { children, innerProps, data } = props;
|
||||
|
||||
if (optionIsFixed(data) && data.isFixed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
role="button"
|
||||
className="text-grey-40"
|
||||
>
|
||||
{children || <CrossIcon size={16} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { GroupBase, PlaceholderProps } from "react-select"
|
||||
|
||||
const Placeholder = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
children,
|
||||
className,
|
||||
cx,
|
||||
}: PlaceholderProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
placeholder: true,
|
||||
},
|
||||
clsx(
|
||||
"absolute top-1/2 -translate-y-1/2 select-none inter-base-regular text-grey-50",
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { components as SelectPrimitives } from "react-select"
|
||||
|
||||
export default SelectPrimitives
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { GroupBase, SingleValueProps } from "react-select"
|
||||
import { hasPrefix } from "../utils"
|
||||
|
||||
const SingleValue = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
children,
|
||||
cx,
|
||||
className,
|
||||
isDisabled,
|
||||
data,
|
||||
}: SingleValueProps<Option, IsMulti, Group>) => {
|
||||
const prefix = hasPrefix(data) ? data.prefix : null
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"single-value": true,
|
||||
"single-value--is-disabled": isDisabled,
|
||||
},
|
||||
clsx(
|
||||
"overflow-hidden absolute top-1/2 -translate-y-1/2 whitespace-nowrap overflow-ellipsis",
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-x-xsmall inter-base-regular">
|
||||
{prefix && <span className="inter-base-semibold">{prefix}</span>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleValue
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import type { MutableRefObject, ReactElement, RefAttributes } from "react"
|
||||
import React, { forwardRef } from "react"
|
||||
import type { GroupBase, SelectInstance } from "react-select"
|
||||
import type { CreatableProps } from "react-select/creatable"
|
||||
import CreatableReactSelect from "react-select/creatable"
|
||||
import { AdjacentContainer } from "../components"
|
||||
import { useSelectProps } from "../use-select-props"
|
||||
|
||||
export type CreatableSelectComponent = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: CreatableProps<Option, IsMulti, Group> &
|
||||
RefAttributes<SelectInstance<Option, IsMulti, Group>>
|
||||
) => ReactElement
|
||||
|
||||
const CreatableSelect = forwardRef(
|
||||
<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
||||
props: CreatableProps<Option, IsMulti, Group>,
|
||||
ref:
|
||||
| ((instance: SelectInstance<Option, IsMulti, Group> | null) => void)
|
||||
| MutableRefObject<SelectInstance<Option, IsMulti, Group> | null>
|
||||
| null
|
||||
) => {
|
||||
const { label, helperText, required, ...rest } = useSelectProps(props)
|
||||
|
||||
return (
|
||||
<AdjacentContainer
|
||||
label={label}
|
||||
helperText={helperText}
|
||||
required={required}
|
||||
>
|
||||
<CreatableReactSelect ref={ref} {...rest} />
|
||||
</AdjacentContainer>
|
||||
)
|
||||
}
|
||||
) as CreatableSelectComponent
|
||||
|
||||
export default CreatableSelect
|
||||
@@ -0,0 +1,4 @@
|
||||
import NextCreateableSelect from "./createable-select"
|
||||
import NextSelect from "./select"
|
||||
|
||||
export { NextSelect, NextCreateableSelect }
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ComponentProps } from "react"
|
||||
import { GroupBase } from "react-select"
|
||||
|
||||
export type SelectSize = "sm" | "md"
|
||||
|
||||
type SelectComponent = "control" | "inner_control" | "menu"
|
||||
|
||||
type SelectComponentStyles = Partial<
|
||||
Record<SelectComponent, ComponentProps<"div">["className"]>
|
||||
>
|
||||
|
||||
declare module "react-select/dist/declarations/src/Select" {
|
||||
export interface Props<
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
> {
|
||||
/**
|
||||
* An optional label to display above the select.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
label?: string
|
||||
/**
|
||||
* An optional flag to indicate if the select is required.
|
||||
* If set to `true`, an asterisk will be displayed next to the label.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
required?: boolean
|
||||
/**
|
||||
* An optional string to display when multiple options are selected in a Select where isMulti is true.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
selectedPlaceholder?: string
|
||||
/**
|
||||
* An optional flag to indicate if the size of the select.
|
||||
*
|
||||
* @defaultValue `"md"`
|
||||
*/
|
||||
size?: SelectSize
|
||||
/**
|
||||
* An optinal helper text to display below the select.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
helperText?: string
|
||||
/**
|
||||
* Errors provided by a containing form.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
errors?: Record<string, unknown>
|
||||
/**
|
||||
* An optional flag to indicate if the select should be able to select all options.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
selectAll?: boolean
|
||||
/**
|
||||
* An optinal object that can be used to override the default styles of the select components.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
customStyles?: SelectComponentStyles
|
||||
/**
|
||||
*
|
||||
* @defaultValue false
|
||||
*
|
||||
*/
|
||||
truncateOption?: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactElement,
|
||||
RefAttributes,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react"
|
||||
import type { GroupBase, Props, SelectInstance } from "react-select"
|
||||
import ReactSelect from "react-select"
|
||||
import { ModalContext } from "../../../modal"
|
||||
import { AdjacentContainer } from "../components"
|
||||
import { useSelectProps } from "../use-select-props"
|
||||
|
||||
export type SelectComponent = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = true,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: Props<Option, IsMulti, Group> &
|
||||
RefAttributes<SelectInstance<Option, IsMulti, Group>>
|
||||
) => ReactElement
|
||||
|
||||
const Select = forwardRef(
|
||||
<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
||||
props: Props<Option, IsMulti, Group>,
|
||||
ref:
|
||||
| ((instance: SelectInstance<Option, IsMulti, Group> | null) => void)
|
||||
| MutableRefObject<SelectInstance<Option, IsMulti, Group> | null>
|
||||
| null
|
||||
) => {
|
||||
const selectProps = useSelectProps(props)
|
||||
|
||||
const { label, required, helperText, name, errors } = selectProps
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { portalRef } = useContext(ModalContext)
|
||||
|
||||
return (
|
||||
<AdjacentContainer
|
||||
ref={containerRef}
|
||||
label={label}
|
||||
htmlFor={name}
|
||||
helperText={helperText}
|
||||
required={required}
|
||||
name={name}
|
||||
errors={errors}
|
||||
>
|
||||
<ReactSelect
|
||||
aria-labelledby={`${name}_label`}
|
||||
ref={ref}
|
||||
name={name}
|
||||
{...selectProps}
|
||||
menuPortalTarget={portalRef?.current?.lastChild || null}
|
||||
menuShouldBlockScroll={true}
|
||||
/>
|
||||
</AdjacentContainer>
|
||||
)
|
||||
}
|
||||
) as SelectComponent
|
||||
|
||||
export default Select
|
||||
@@ -0,0 +1,85 @@
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"
|
||||
import Components from "./components"
|
||||
import { formatOptionLabel, hasLabel } from "./utils"
|
||||
|
||||
export const useSelectProps = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
components = {},
|
||||
isMulti,
|
||||
closeMenuOnScroll = true,
|
||||
hideSelectedOptions = false,
|
||||
closeMenuOnSelect,
|
||||
label,
|
||||
size,
|
||||
options,
|
||||
onChange: changeFn,
|
||||
styles,
|
||||
...props
|
||||
}: Props<Option, IsMulti, Group>): Props<Option, IsMulti, Group> => {
|
||||
const [stateOptions, setStateOptions] = useState(options || [])
|
||||
|
||||
const sortOptions = (values: Option[]) => {
|
||||
const tmp = values || []
|
||||
|
||||
const unselectedOptions = stateOptions.filter(
|
||||
(option) => !tmp.find((op) => isEqual(op, option))
|
||||
)
|
||||
|
||||
const orderedNewOptions = tmp.sort((a, b) => {
|
||||
if (hasLabel(a) && hasLabel(b)) {
|
||||
return a.label > b.label ? 1 : b.label > a.label ? -1 : 0
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
setStateOptions(orderedNewOptions.concat(unselectedOptions as Option[]))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isMulti && options) {
|
||||
sortOptions(props.value as Option[])
|
||||
} else {
|
||||
setStateOptions(options || [])
|
||||
}
|
||||
}, [options, props.value, isMulti])
|
||||
|
||||
const onChange = (
|
||||
newValue: OnChangeValue<Option, IsMulti>,
|
||||
actionMeta: ActionMeta<Option>
|
||||
) => {
|
||||
if (isMulti) {
|
||||
sortOptions(newValue as Option[])
|
||||
}
|
||||
|
||||
if (changeFn) {
|
||||
changeFn(newValue, actionMeta)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
components: Components,
|
||||
styles: {
|
||||
menuPortal: (base) => ({ ...base, zIndex: 60 }),
|
||||
...styles,
|
||||
},
|
||||
isMulti,
|
||||
closeMenuOnScroll: true,
|
||||
closeMenuOnSelect:
|
||||
closeMenuOnSelect !== undefined ? closeMenuOnSelect : isMulti !== true,
|
||||
hideSelectedOptions,
|
||||
menuPosition: "fixed",
|
||||
maxMenuHeight: size === "sm" ? 154 : 188,
|
||||
formatOptionLabel,
|
||||
size,
|
||||
options: stateOptions,
|
||||
onChange,
|
||||
...props,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { omit } from "lodash"
|
||||
import Highlighter from "react-highlight-words"
|
||||
import type {
|
||||
CommonPropsAndClassName,
|
||||
FormatOptionLabelMeta,
|
||||
GroupBase,
|
||||
} from "react-select"
|
||||
|
||||
export const cleanCommonProps = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>,
|
||||
AdditionalProps
|
||||
>(
|
||||
props: Partial<CommonPropsAndClassName<Option, IsMulti, Group>> &
|
||||
AdditionalProps
|
||||
) => {
|
||||
const innerProps = omit(props, [
|
||||
"className",
|
||||
"clearValue",
|
||||
"cx",
|
||||
"getStyles",
|
||||
"getValue",
|
||||
"hasValue",
|
||||
"isMulti",
|
||||
"isRtl",
|
||||
"options",
|
||||
"selectOption",
|
||||
"selectProps",
|
||||
"setValue",
|
||||
"theme",
|
||||
])
|
||||
return { ...innerProps }
|
||||
}
|
||||
|
||||
export const optionIsFixed = (
|
||||
option: unknown
|
||||
): option is { isFixed: unknown } =>
|
||||
typeof option === "object" && option !== null && "isFixed" in option
|
||||
|
||||
export const optionIsDisabled = (
|
||||
option: unknown
|
||||
): option is { isDisabled: boolean } =>
|
||||
typeof option === "object" && option !== null && "isDisabled" in option
|
||||
|
||||
export const hasLabel = (option: unknown): option is { label: string } => {
|
||||
return typeof option === "object" && option !== null && "label" in option
|
||||
}
|
||||
|
||||
export const hasPrefix = (option: unknown): option is { prefix: string } => {
|
||||
return typeof option === "object" && option !== null && "prefix" in option
|
||||
}
|
||||
|
||||
export const hasSuffix = (option: unknown): option is { suffix: string } => {
|
||||
return typeof option === "object" && option !== null && "suffix" in option
|
||||
}
|
||||
|
||||
export const isCreateOption = (
|
||||
option: unknown
|
||||
): option is { __isNew__: true } => {
|
||||
return typeof option === "object" && option !== null && "__isNew__" in option
|
||||
}
|
||||
|
||||
export const formatOptionLabel = <Option,>(
|
||||
option: Option,
|
||||
{ inputValue }: FormatOptionLabelMeta<Option>
|
||||
) => {
|
||||
if (!hasLabel(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCreateOption(option)) {
|
||||
return option.label
|
||||
}
|
||||
|
||||
return (
|
||||
<Highlighter
|
||||
searchWords={[inputValue]}
|
||||
textToHighlight={option.label}
|
||||
highlightClassName="bg-orange-10"
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user