feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)

This commit is contained in:
Kasper Fabricius Kristensen
2023-03-03 10:09:16 +01:00
committed by GitHub
parent d6b1ad1ccd
commit 40de54b010
928 changed files with 85441 additions and 384 deletions
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
);
};
@@ -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
@@ -0,0 +1,3 @@
import { components as SelectPrimitives } from "react-select"
export default SelectPrimitives
@@ -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
@@ -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 }
@@ -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"
/>
)
}