1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

Upgrade react-select & improve select typing

This commit is contained in:
thatmattlove
2021-12-06 10:53:15 -07:00
parent 7c73b2b9a1
commit 6afe23bd17
17 changed files with 554 additions and 429 deletions

View File

@@ -5,11 +5,16 @@ import { useFormContext } from 'react-hook-form';
import { Select } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useOpposingColor, useFormState } from '~/hooks';
import { isMultiValue, isSingleValue } from '~/components/select';
import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types';
import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField, LocationCardProps } from './types';
function buildOptions(devices: DeviceGroup[]): OptionGroup[] {
/** Location option type alias for future extensions. */
type LocationOption = SingleOption;
function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
return devices
.map(group => {
const label = group.group;
@@ -39,7 +44,7 @@ const LocationCard = (props: LocationCardProps): JSX.Element => {
const { label } = option;
const [isChecked, setChecked] = useState(defaultChecked);
function handleChange(value: SingleOption) {
function handleChange(value: LocationOption) {
if (isChecked) {
setChecked(false);
onChange('remove', value);
@@ -176,15 +181,15 @@ export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
* @param options Final value. React-select determines if an option is being added or removed and
* only sends back the final value.
*/
function handleSelectChange(options: SingleOption[] | SingleOption): void {
if (Array.isArray(options)) {
const handleSelectChange: SelectOnChange<LocationOption> = (options): void => {
if (isMultiValue(options)) {
onChange({ field: 'queryLocation', value: options.map(o => o.value) });
setSelection('queryLocation', options);
} else {
setSelection<LocationOption>('queryLocation', options);
} else if (isSingleValue(options)) {
onChange({ field: 'queryLocation', value: options.value });
setSelection('queryLocation', [options]);
setSelection<LocationOption>('queryLocation', [options]);
}
}
};
if (element === 'cards') {
return (
@@ -211,9 +216,8 @@ export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
);
} else if (element === 'select') {
return (
<Select
<Select<LocationOption, true>
isMulti
size="lg"
options={options}
aria-label={label}
name="queryLocation"

View File

@@ -2,40 +2,44 @@ import { useMemo } from 'react';
import { Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react';
import { components } from 'react-select';
import { If, Select } from '~/components';
import { isSingleValue } from '~/components/select';
import { useColorValue } from '~/context';
import { useDirective, useFormState } from '~/hooks';
import { isSelectDirective } from '~/types';
import { UserIP } from './userIP';
import type { OptionProps } from 'react-select';
import type { OptionProps, GroupBase } from 'react-select';
import type { SelectOnChange } from '~/components/select';
import type { Directive, SingleOption } from '~/types';
import type { TQueryTarget } from './types';
function buildOptions(directive: Nullable<Directive>): SingleOption[] {
type OptionWithDescription = SingleOption<{ description: string | null }>;
function buildOptions(directive: Nullable<Directive>): OptionWithDescription[] {
if (directive !== null && isSelectDirective(directive)) {
return directive.options.map(o => ({
value: o.value,
label: o.name,
description: o.description,
data: { description: o.description },
}));
}
return [];
}
const Option = (props: OptionProps<Dict, false>) => {
const Option = (props: OptionProps<OptionWithDescription, false>) => {
const { label, data } = props;
return (
<components.Option {...props}>
<components.Option<OptionWithDescription, false, GroupBase<OptionWithDescription>> {...props}>
<Text as="span">{label}</Text>
<br />
<Text fontSize="xs" as="span">
{data.description}
{data.data?.description}
</Text>
</components.Option>
);
};
export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
export const QueryTarget = (props: TQueryTarget): JSX.Element => {
const { name, register, onChange, placeholder } = props;
const bg = useColorValue('white', 'whiteAlpha.100');
@@ -54,22 +58,20 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
onChange({ field: name, value: e.target.value });
}
function handleSelectChange(e: SingleOption | SingleOption[]): void {
if (!Array.isArray(e) && e !== null) {
const handleSelectChange: SelectOnChange<OptionWithDescription> = e => {
if (isSingleValue(e)) {
onChange({ field: name, value: e.value });
setTarget({ display: e.value });
}
}
};
return (
<>
<input {...register('queryTarget')} hidden readOnly value={form.queryTarget} />
<If c={directive !== null && isSelectDirective(directive)}>
<Select
size="lg"
<Select<OptionWithDescription, false>
name={name}
options={options}
innerRef={register}
components={{ Option }}
onChange={handleSelectChange}
/>

View File

@@ -4,22 +4,25 @@ import { Box, Button, HStack, useRadio, useRadioGroup } from '@chakra-ui/react';
import { useFormContext } from 'react-hook-form';
import { components } from 'react-select';
import { Select } from '~/components';
import { useFormState } from '~/hooks';
import { useFormState, useFormSelections } from '~/hooks';
import { isSingleValue } from '~/components/select';
import type { UseRadioProps } from '@chakra-ui/react';
import type { MenuListComponentProps } from 'react-select';
import type { SingleOption, OptionGroup, SelectOption } from '~/types';
import type { TOptions } from '~/components/select';
import type { MenuListProps } from 'react-select';
import type { SingleOption, OptionGroup, OptionsOrGroup } from '~/types';
import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField } from './types';
function sorter<T extends SingleOption | OptionGroup>(a: T, b: T): number {
type QueryTypeOption = SingleOption<{ group?: string }>;
function sorter<T extends QueryTypeOption | OptionGroup<QueryTypeOption>>(a: T, b: T): number {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
}
type UserFilter = {
selected: string;
setSelected(n: string): void;
filter(candidate: SelectOption<{ group: string | null }>, input: string): boolean;
filter(candidate: QueryTypeOption, input: string): boolean;
};
const useFilter = create<UserFilter>((set, get) => ({
@@ -28,10 +31,9 @@ const useFilter = create<UserFilter>((set, get) => ({
set(() => ({ selected: newValue }));
},
filter(candidate, input): boolean {
const {
label,
data: { group },
} = candidate;
const { label, data } = candidate;
const group = data?.group ?? null;
if (input && (label || group)) {
const search = input.toLowerCase();
if (group) {
@@ -52,14 +54,14 @@ const useFilter = create<UserFilter>((set, get) => ({
function useOptions() {
const filtered = useFormState(s => s.filtered);
return useMemo((): TOptions => {
return useMemo((): OptionsOrGroup<QueryTypeOption> => {
const groupNames = new Set(
filtered.types
.filter(t => t.groups.length > 0)
.map(t => t.groups)
.flat(),
);
const optGroups: OptionGroup[] = Array.from(groupNames).map(group => ({
const optGroups: OptionGroup<QueryTypeOption>[] = Array.from(groupNames).map(group => ({
label: group,
options: filtered.types
.filter(t => t.groups.includes(group))
@@ -67,7 +69,7 @@ function useOptions() {
.sort(sorter),
}));
const noGroups: OptionGroup = {
const noGroups: OptionGroup<QueryTypeOption> = {
label: '',
options: filtered.types
.filter(t => t.groups.length === 0)
@@ -108,7 +110,7 @@ const GroupFilter = (props: React.PropsWithChildren<UseRadioProps>): JSX.Element
);
};
const MenuList = (props: MenuListComponentProps<TOptions, false>) => {
const MenuList = (props: MenuListProps<QueryTypeOption, boolean>): JSX.Element => {
const { children, ...rest } = props;
const filtered = useFormState(s => s.filtered);
const selected = useFilter(state => state.selected);
@@ -150,27 +152,25 @@ export const QueryType = (props: TQuerySelectField): JSX.Element => {
formState: { errors },
} = useFormContext();
const setSelection = useFormState(s => s.setSelection);
const selections = useFormState(s => s.selections);
const selections = useFormSelections<QueryTypeOption>();
const setFormValue = useFormState(s => s.setFormValue);
const options = useOptions();
const { filter } = useFilter(); // Intentionally re-render on any changes
function handleChange(e: SingleOption | SingleOption[]): void {
const handleChange: SelectOnChange<QueryTypeOption> = e => {
let value = '';
if (!Array.isArray(e) && e !== null) {
// setFormValue('queryType', e.value);
setSelection('queryType', e);
if (isSingleValue(e)) {
setSelection<QueryTypeOption>('queryType', e);
value = e.value;
} else {
setFormValue('queryType', '');
setSelection('queryType', null);
setSelection<QueryTypeOption>('queryType', null);
}
onChange({ field: 'queryType', value });
}
};
return (
<Select
size="lg"
<Select<QueryTypeOption>
name="queryType"
options={options}
aria-label={label}

View File

@@ -160,6 +160,7 @@ export const LookingGlass = (): JSX.Element => {
} else if (e.field === 'queryTarget' && isString(e.value)) {
setFormValue('queryTarget', e.value);
}
console.table(form);
}
useEffect(() => {

View File

@@ -1,2 +1,3 @@
export * from './select';
export type { TOptions } from './types';
export { isSingleValue, isMultiValue } from './types';
export type { SelectOnChange } from './types';

View File

@@ -1,16 +1,17 @@
import { Badge, Box, HStack } from '@chakra-ui/react';
import { Badge, chakra, HStack } from '@chakra-ui/react';
import { components } from 'react-select';
import type { TOption } from './types';
import type { OptionProps, GroupBase } from 'react-select';
import type { SingleOption } from '~/types';
export const Option = (props: TOption): JSX.Element => {
export const Option = <Opt extends SingleOption, IsMulti extends boolean>(
props: OptionProps<Opt, IsMulti>,
): JSX.Element => {
const { label, data } = props;
const tags = Array.isArray(data.tags) ? (data.tags as string[]) : [];
return (
<components.Option {...props}>
<Box as="span" d={{ base: 'block', lg: 'inline' }}>
{label}
</Box>
<components.Option<Opt, IsMulti, GroupBase<Opt>> {...props}>
<chakra.span d={{ base: 'block', lg: 'inline' }}>{label}</chakra.span>
{tags.length > 0 && (
<HStack d={{ base: 'flex', lg: 'inline-flex' }} ms={{ base: 0, lg: 2 }} alignItems="center">
{tags.map(tag => (

View File

@@ -1,6 +1,6 @@
import { createContext, useContext, useMemo } from 'react';
import { createContext, forwardRef, useContext } from 'react';
import ReactSelect from 'react-select';
import { chakra, useDisclosure } from '@chakra-ui/react';
import { useDisclosure } from '@chakra-ui/react';
import { useColorMode } from '~/context';
import { Option } from './option';
import {
@@ -17,67 +17,81 @@ import {
useMultiValueRemoveStyle,
useIndicatorSeparatorStyle,
} from './styles';
import { isSingleValue } from './types';
import type {
Props as ReactSelectProps,
MultiValue,
OnChangeValue,
SelectInstance,
} from 'react-select';
import type { SingleOption } from '~/types';
import type { TSelectBase, TSelectContext, TReactSelectChakra } from './types';
import type { TSelectBase, TSelectContext } from './types';
const SelectContext = createContext<TSelectContext>({} as TSelectContext);
export const useSelectContext = (): TSelectContext => useContext(SelectContext);
const ReactSelectChakra = chakra<typeof ReactSelect, TReactSelectChakra>(ReactSelect);
export const Select = forwardRef(
<Opt extends SingleOption = SingleOption, IsMulti extends boolean = boolean>(
props: TSelectBase<Opt, IsMulti>,
ref: React.Ref<SelectInstance<Opt, IsMulti>>,
): JSX.Element => {
const { options, isMulti, onSelect, isError = false, components, ...rest } = props;
export const Select: React.FC<TSelectBase> = (props: TSelectBase) => {
const { options, multi, onSelect, isError = false, components, ...rest } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen, onOpen, onClose } = useDisclosure();
const { colorMode } = useColorMode();
const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>(
() => ({ colorMode, isOpen, isError }),
[colorMode, isError, isOpen],
);
const defaultOnChange: ReactSelectProps<Opt, IsMulti>['onChange'] = changed => {
if (isSingleValue<Opt>(changed)) {
changed = [changed] as unknown as OnChangeValue<Opt, IsMulti>;
}
if (typeof onSelect === 'function') {
onSelect(changed as MultiValue<Opt>);
}
};
const defaultOnChange = (changed: SingleOption | SingleOption[]) => {
if (!Array.isArray(changed)) {
changed = [changed];
}
if (typeof onSelect === 'function') {
onSelect(changed);
}
};
const menu = useMenuStyle<Opt, IsMulti>({ colorMode });
const menuList = useMenuListStyle<Opt, IsMulti>({ colorMode });
const control = useControlStyle<Opt, IsMulti>({ colorMode });
const option = useOptionStyle<Opt, IsMulti>({ colorMode });
const singleValue = useSingleValueStyle<Opt, IsMulti>({ colorMode });
const multiValue = useMultiValueStyle<Opt, IsMulti>({ colorMode });
const multiValueLabel = useMultiValueLabelStyle<Opt, IsMulti>({ colorMode });
const multiValueRemove = useMultiValueRemoveStyle<Opt, IsMulti>({ colorMode });
const menuPortal = useMenuPortal<Opt, IsMulti>();
const placeholder = usePlaceholderStyle<Opt, IsMulti>({ colorMode });
const indicatorSeparator = useIndicatorSeparatorStyle<Opt, IsMulti>({ colorMode });
const rsTheme = useRSTheme();
const multiValue = useMultiValueStyle({ colorMode });
const multiValueLabel = useMultiValueLabelStyle({ colorMode });
const multiValueRemove = useMultiValueRemoveStyle({ colorMode });
const menuPortal = useMenuPortal();
const rsTheme = useRSTheme();
return (
<SelectContext.Provider value={selectContext}>
<ReactSelectChakra
onChange={defaultOnChange}
onMenuClose={onClose}
onMenuOpen={onOpen}
isClearable={true}
options={options}
isMulti={multi}
theme={rsTheme}
components={{ Option, ...components }}
styles={{
menuPortal,
multiValue,
multiValueLabel,
multiValueRemove,
menu: useMenuStyle,
option: useOptionStyle,
control: useControlStyle,
menuList: useMenuListStyle,
singleValue: useSingleValueStyle,
placeholder: usePlaceholderStyle,
indicatorSeparator: useIndicatorSeparatorStyle,
}}
{...rest}
/>
</SelectContext.Provider>
);
};
return (
<SelectContext.Provider value={{ colorMode, isOpen, isError }}>
<ReactSelect<Opt, IsMulti>
onChange={defaultOnChange}
onMenuClose={onClose}
onMenuOpen={onOpen}
isClearable={true}
options={options}
isMulti={isMulti}
theme={rsTheme}
components={{ Option, ...components }}
ref={ref}
styles={{
menu,
option,
control,
menuList,
menuPortal,
multiValue,
singleValue,
placeholder,
multiValueLabel,
multiValueRemove,
indicatorSeparator,
}}
{...rest}
/>
</SelectContext.Provider>
);
},
);

View File

@@ -0,0 +1,219 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback } from 'react';
import { useToken } from '@chakra-ui/react';
import { mergeWith } from '@chakra-ui/utils';
import { merge } from 'merge-anything';
import { useOpposingColor, useOpposingColorCallback } from '~/hooks';
import { useColorValue, useColorToken, useMobile } from '~/context';
import { useSelectContext } from './select';
import * as ReactSelect from 'react-select';
import type { SingleOption } from '~/types';
import type { RSStyleCallbackProps, RSThemeFunction, RSStyleFunction } from './types';
export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'control', Opt, IsMulti> => {
const { colorMode } = props;
const { isError } = useSelectContext();
const minHeight = useToken('space', 12);
const borderRadius = useToken('radii', 'md');
const color = useColorToken('colors', 'black', 'whiteAlpha.800');
const focusBorder = useColorToken('colors', 'blue.500', 'blue.300');
const invalidBorder = useColorToken('colors', 'red.500', 'red.300');
const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50');
const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400');
const backgroundColor = useColorToken('colors', 'white', 'whiteAlpha.100');
return useCallback(
(base, state) => {
const { isFocused } = state;
const styles = {
backgroundColor,
borderRadius,
color,
minHeight,
transition: 'all 0.2s',
borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor,
boxShadow: isError
? `0 0 0 1px ${invalidBorder}`
: isFocused
? `0 0 0 1px ${focusBorder}`
: undefined,
'&:hover': { borderColor: isFocused ? focusBorder : borderHover },
'&:hover > div > span': { backgroundColor: borderHover },
'&:focus': { borderColor: isError ? invalidBorder : focusBorder },
'&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` },
};
return mergeWith({}, base, styles);
},
[colorMode, isError],
);
};
export const useMenuStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'menu', Opt, IsMulti> => {
const { colorMode } = props;
const { isOpen } = useSelectContext();
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const styles = { backgroundColor, zIndex: 1500 };
return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]);
};
export const useMenuListStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'menuList', Opt, IsMulti> => {
const { colorMode } = props;
const { isOpen } = useSelectContext();
const borderRadius = useToken('radii', 'md');
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50');
const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300');
const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
const styles = {
borderRadius,
backgroundColor,
'&::-webkit-scrollbar': { width: '5px' },
'&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack },
'&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover },
'-ms-overflow-style': { display: 'none' },
};
return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]);
};
export const useOptionStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'option', Opt, IsMulti> => {
const { colorMode } = props;
const { isOpen } = useSelectContext();
const fontSize = useToken('fontSizes', 'lg');
const disabled = useToken('colors', 'whiteAlpha.400');
const active = useColorToken('colors', 'primary.600', 'primary.400');
const focused = useColorToken('colors', 'primary.500', 'primary.300');
const selected = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
const activeColor = useOpposingColor(active);
const getColor = useOpposingColorCallback();
return useCallback(
(base, state) => {
const { isFocused, isSelected, isDisabled } = state;
let backgroundColor = 'transparent';
switch (true) {
case isDisabled:
backgroundColor = disabled;
break;
case isSelected:
backgroundColor = selected;
break;
case isFocused:
backgroundColor = focused;
break;
}
const color = getColor(backgroundColor);
const styles = {
color: backgroundColor === 'transparent' ? 'currentColor' : color,
'&:active': { backgroundColor: active, color: activeColor },
'&:focus': { backgroundColor: active, color: activeColor },
backgroundColor,
fontSize,
};
return mergeWith({}, base, styles);
},
[isOpen, colorMode],
);
};
export const useIndicatorSeparatorStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'whiteAlpha.700', 'gray.600');
const styles = { backgroundColor };
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
};
export const usePlaceholderStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'placeholder', Opt, IsMulti> => {
const { colorMode } = props;
const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700');
const fontSize = useToken('fontSizes', 'lg');
return useCallback(base => mergeWith({}, base, { color, fontSize }), [colorMode]);
};
export const useSingleValueStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'singleValue', Opt, IsMulti> => {
const { colorMode } = props;
const color = useColorValue('black', 'whiteAlpha.800');
const fontSize = useToken('fontSizes', 'lg');
const styles = { color, fontSize };
return useCallback(base => mergeWith({}, base, styles), [color, colorMode]);
};
export const useMultiValueStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'multiValue', Opt, IsMulti> => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = { backgroundColor, color };
return useCallback(base => mergeWith({}, base, styles), [backgroundColor, colorMode]);
};
export const useMultiValueLabelStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'multiValueLabel', Opt, IsMulti> => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = { color };
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
};
export const useMultiValueRemoveStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps,
): RSStyleFunction<'multiValueRemove', Opt, IsMulti> => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = {
color,
'&:hover': { backgroundColor: 'inherit', color, opacity: 0.7 },
};
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
};
export const useRSTheme = (): RSThemeFunction => {
const borderRadius = useToken('radii', 'md');
return useCallback((t: ReactSelect.Theme): ReactSelect.Theme => ({ ...t, borderRadius }), []);
};
export const useMenuPortal = <Opt extends SingleOption, IsMulti extends boolean>(): RSStyleFunction<
'menuPortal',
Opt,
IsMulti
> => {
const isMobile = useMobile();
const styles = {
zIndex: isMobile ? 1500 : 1,
};
return useCallback(base => merge(base, styles), [isMobile]);
};

View File

@@ -1,192 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useMemo } from 'react';
import { useToken } from '@chakra-ui/react';
import { mergeWith } from '@chakra-ui/utils';
import { useOpposingColor } from '~/hooks';
import { useColorValue, useColorToken, useMobile } from '~/context';
import { useSelectContext } from './select';
import type {
TMenu,
TOption,
TStyles,
TControl,
TRSTheme,
TMultiValue,
TRSThemeCallback,
TRSStyleCallback,
} from './types';
export const useControlStyle = (base: TStyles, state: TControl): TStyles => {
const { isFocused } = state;
const { colorMode, isError } = useSelectContext();
const minHeight = useToken('space', 12);
const borderRadius = useToken('radii', 'md');
const color = useColorToken('colors', 'black', 'whiteAlpha.800');
const focusBorder = useColorToken('colors', 'blue.500', 'blue.300');
const invalidBorder = useColorToken('colors', 'red.500', 'red.300');
const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50');
const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400');
const backgroundColor = useColorToken('colors', 'white', 'whiteAlpha.100');
const styles = {
backgroundColor,
borderRadius,
color,
minHeight,
transition: 'all 0.2s',
borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor,
boxShadow: isError
? `0 0 0 1px ${invalidBorder}`
: isFocused
? `0 0 0 1px ${focusBorder}`
: undefined,
'&:hover': { borderColor: isFocused ? focusBorder : borderHover },
'&:hover > div > span': { backgroundColor: borderHover },
'&:focus': { borderColor: isError ? invalidBorder : focusBorder },
'&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` },
};
return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused, isError]);
};
export const useMenuStyle = (base: TStyles, _: TMenu): TStyles => {
const { colorMode, isOpen } = useSelectContext();
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const styles = { backgroundColor };
return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]);
};
export const useMenuListStyle = (base: TStyles): TStyles => {
const { colorMode, isOpen } = useSelectContext();
const borderRadius = useToken('radii', 'md');
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50');
const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300');
const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
const styles = {
borderRadius,
backgroundColor,
'&::-webkit-scrollbar': { width: '5px' },
'&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack },
'&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover },
'-ms-overflow-style': { display: 'none' },
};
return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]);
};
export const useOptionStyle = (base: TStyles, state: TOption): TStyles => {
const { isFocused, isSelected, isDisabled } = state;
const { colorMode, isOpen } = useSelectContext();
const fontSize = useToken('fontSizes', 'lg');
const disabled = useToken('colors', 'whiteAlpha.400');
const active = useColorToken('colors', 'primary.600', 'primary.400');
const focused = useColorToken('colors', 'primary.500', 'primary.300');
const selected = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
const activeColor = useOpposingColor(active);
const backgroundColor = useMemo(() => {
let bg = 'transparent';
switch (true) {
case isDisabled:
bg = disabled;
break;
case isSelected:
bg = selected;
break;
case isFocused:
bg = focused;
break;
}
return bg;
}, [isDisabled, isFocused, isSelected]);
const color = useOpposingColor(backgroundColor);
const styles = {
color: backgroundColor === 'transparent' ? 'currentColor' : color,
'&:active': { backgroundColor: active, color: activeColor },
'&:focus': { backgroundColor: active, color: activeColor },
backgroundColor,
fontSize,
};
return useMemo(
() => mergeWith({}, base, styles),
[isOpen, colorMode, isFocused, isDisabled, isSelected],
);
};
export const useIndicatorSeparatorStyle = (base: TStyles): TStyles => {
const { colorMode } = useSelectContext();
const backgroundColor = useColorToken('colors', 'whiteAlpha.700', 'gray.600');
const styles = { backgroundColor };
return useMemo(() => mergeWith({}, base, styles), [colorMode]);
};
export const usePlaceholderStyle = (base: TStyles): TStyles => {
const { colorMode } = useSelectContext();
const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700');
const fontSize = useToken('fontSizes', 'lg');
return useMemo(() => mergeWith({}, base, { color, fontSize }), [colorMode]);
};
export const useSingleValueStyle = (): TRSStyleCallback => {
const { colorMode } = useSelectContext();
const color = useColorValue('black', 'whiteAlpha.800');
const fontSize = useToken('fontSizes', 'lg');
const styles = { color, fontSize };
return useCallback((base: TStyles) => mergeWith({}, base, styles), [color, colorMode]);
};
export const useMultiValueStyle = (props: TMultiValue): TRSStyleCallback => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = { backgroundColor, color };
return useCallback((base: TStyles) => mergeWith({}, base, styles), [backgroundColor, colorMode]);
};
export const useMultiValueLabelStyle = (props: TMultiValue): TRSStyleCallback => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = { color };
return useCallback((base: TStyles) => mergeWith({}, base, styles), [colorMode]);
};
export const useMultiValueRemoveStyle = (props: TMultiValue): TRSStyleCallback => {
const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor);
const styles = {
color,
'&:hover': { backgroundColor: 'inherit', color, opacity: 0.7 },
};
return useCallback((base: TStyles) => mergeWith({}, base, styles), [colorMode]);
};
export const useRSTheme = (): TRSThemeCallback => {
const borderRadius = useToken('radii', 'md');
return useCallback((t: TRSTheme): TRSTheme => ({ ...t, borderRadius }), []);
};
export const useMenuPortal = (): TRSStyleCallback => {
const isMobile = useMobile();
const styles = {
zIndex: isMobile ? 1500 : 1,
};
return useCallback((base: TStyles) => mergeWith({}, base, styles), [isMobile]);
};

View File

@@ -1,38 +1,20 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */
/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */
import * as ReactSelect from 'react-select';
import type {
Props as IReactSelect,
ControlProps,
MenuProps,
MenuListComponentProps,
OptionProps,
MultiValueProps,
IndicatorProps,
Theme as RSTheme,
PlaceholderProps,
Styles as RSStyles,
} from 'react-select';
import type { BoxProps } from '@chakra-ui/react';
import type { Theme, SingleOption, OptionGroup } from '~/types';
import type { StylesProps, StylesConfigFunction } from 'react-select/dist/declarations/src/styles';
import type { Theme, SingleOption } from '~/types';
export interface TSelectState {
[k: string]: string[];
}
export type SelectOnChange<
Opt extends SingleOption = SingleOption,
IsMulti extends boolean = boolean,
> = NonNullable<ReactSelect.Props<Opt, IsMulti>['onChange']>;
export type TOptions = Array<SingleOption | OptionGroup>;
export type TReactSelectChakra = Omit<IReactSelect, 'isMulti' | 'onSelect' | 'onChange'> &
Omit<BoxProps, 'onChange' | 'onSelect'>;
export interface TSelectBase extends TReactSelectChakra {
export interface TSelectBase<Opt extends SingleOption, IsMulti extends boolean>
extends ReactSelect.Props<Opt, IsMulti> {
name: string;
multi?: boolean;
isMulti?: IsMulti;
isError?: boolean;
options: TOptions;
required?: boolean;
onSelect?: (s: SingleOption[]) => void;
onChange?: (c: SingleOption | SingleOption[]) => void;
onSelect?: (s: ReactSelect.MultiValue<Opt>) => void;
colorScheme?: Theme.ColorNames;
}
@@ -42,40 +24,32 @@ export interface TSelectContext {
isError: boolean;
}
export interface TMultiValueRemoveProps {
children: Node;
data: any;
innerProps: {
className: string;
onTouchEnd: (e: any) => void;
onClick: (e: any) => void;
onMouseDown: (e: any) => void;
};
selectProps: any;
export interface RSStyleCallbackProps {
colorMode: 'light' | 'dark';
}
export interface TRSTheme extends Omit<RSTheme, 'borderRadius'> {
borderRadius: string | number;
type StyleConfigKeys = keyof ReactSelect.StylesConfig<
SingleOption,
boolean,
ReactSelect.GroupBase<SingleOption>
>;
export type RSStyleFunction<
K extends StyleConfigKeys,
Opt extends SingleOption,
IsMulti extends boolean,
> = StylesConfigFunction<StylesProps<Opt, IsMulti, ReactSelect.GroupBase<Opt>>[K]>;
export type RSThemeFunction = (theme: ReactSelect.Theme) => ReactSelect.Theme;
export function isSingleValue<Opt extends SingleOption>(
value: ReactSelect.SingleValue<Opt> | ReactSelect.MultiValue<Opt>,
): value is NonNullable<ReactSelect.SingleValue<Opt>> {
return value !== null && !Array.isArray(value);
}
export type TControl = ControlProps<TOptions, false>;
export type TMenu = MenuProps<TOptions, false>;
export type TMenuList = MenuListComponentProps<TOptions, false>;
export type TOption = OptionProps<TOptions, false>;
export type TMultiValueState = MultiValueProps<TOptions>;
export type TIndicator = IndicatorProps<TOptions, false>;
export type TPlaceholder = PlaceholderProps<TOptions, false>;
export type TMultiValue = Pick<TSelectContext, 'colorMode'>;
export type TRSStyleCallback = (base: TStyles) => TStyles;
export type TRSThemeCallback = (theme: TRSTheme) => TRSTheme;
export type TStyles = RSStyles<TOptions, false>;
export function isMultiValue<Opt extends SingleOption>(
value: ReactSelect.SingleValue<Opt> | ReactSelect.MultiValue<Opt>,
): value is NonNullable<ReactSelect.MultiValue<Opt>> {
return value !== null && Array.isArray(value);
}

View File

@@ -5,8 +5,9 @@ import plur from 'plur';
import isEqual from 'react-fast-compare';
import { all, andJoin, dedupObjectArray, withDev } from '~/util';
import type { SingleValue, MultiValue } from 'react-select';
import type { StateCreator } from 'zustand';
import type { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
import { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
import type { SingleOption, Directive, FormData, Text } from '~/types';
import type { UseDevice } from './types';
@@ -21,9 +22,9 @@ interface FormValues {
/**
* Selected *options*, vs. values.
*/
interface FormSelections {
queryLocation: SingleOption[];
queryType: SingleOption | null;
interface FormSelections<Opt extends SingleOption = SingleOption> {
queryLocation: MultiValue<Opt>;
queryType: SingleValue<Opt>;
}
interface Filtered {
@@ -39,13 +40,13 @@ interface Target {
display: string;
}
interface FormStateType {
interface FormStateType<Opt extends SingleOption = SingleOption> {
// Values
filtered: Filtered;
form: FormValues;
loading: boolean;
responses: Responses;
selections: FormSelections;
selections: FormSelections<Opt>;
status: FormStatus;
target: Target;
resolvedIsOpen: boolean;
@@ -57,7 +58,13 @@ interface FormStateType {
addResponse(deviceId: string, data: QueryResponse): void;
setLoading(value: boolean): void;
setStatus(value: FormStatus): void;
setSelection<K extends keyof FormSelections>(field: K, value: FormSelections[K]): void;
setSelection<
Opt extends SingleOption,
K extends keyof FormSelections<Opt> = keyof FormSelections<Opt>,
>(
field: K,
value: FormSelections[K],
): void;
setTarget(update: Partial<Target>): void;
getDirective(): Directive | null;
reset(): void;
@@ -95,7 +102,10 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
set({ status });
},
setSelection<K extends keyof FormSelections>(field: K, value: FormSelections[K]): void {
setSelection<
Opt extends SingleOption,
K extends keyof FormSelections<Opt> = keyof FormSelections<Opt>,
>(field: K, value: FormSelections[K]): void {
set(state => ({ selections: { ...state.selections, [field]: value } }));
},
@@ -223,6 +233,10 @@ export const useFormState = create<FormStateType>(
withDev<FormStateType>(formState, 'useFormState'),
);
export function useFormSelections<Opt extends SingleOption = SingleOption>(): FormSelections<Opt> {
return useFormState(s => s.selections as FormSelections<Opt>);
}
export function useView(): FormStatus {
const { status, form } = useFormState(({ status, form }) => ({ status, form }));
return useMemo(() => {

View File

@@ -1,25 +1,37 @@
import { useMemo } from 'react';
import { useMemo, useCallback } from 'react';
import { getColor, isLight } from '@chakra-ui/theme-tools';
import { useTheme } from '~/context';
import type { TOpposingOptions } from './types';
export type UseIsDarkCallbackReturn = (color: string) => boolean;
/**
* Parse the color string to determine if it's a Chakra UI theme key, and determine if the
* opposing color should be black or white.
*/
export function useIsDark(color: string): boolean {
const isDarkFn = useIsDarkCallback();
return useMemo((): boolean => isDarkFn(color), [color, isDarkFn]);
}
export function useIsDarkCallback(): UseIsDarkCallbackReturn {
const theme = useTheme();
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
color = getColor(theme, color, color);
}
let opposingShouldBeDark = true;
try {
opposingShouldBeDark = isLight(color)(theme);
} catch (err) {
console.error(err);
}
return opposingShouldBeDark;
return useCallback(
(color: string): boolean => {
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
color = getColor(theme, color, color);
}
let opposingShouldBeDark = true;
try {
opposingShouldBeDark = isLight(color)(theme);
} catch (err) {
console.error(err);
}
return opposingShouldBeDark;
},
[theme],
);
}
/**
@@ -36,3 +48,17 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str
}
}, [isBlack, options?.dark, options?.light]);
}
export function useOpposingColorCallback(options?: TOpposingOptions): (color: string) => string {
const isDark = useIsDarkCallback();
return useCallback(
(color: string) => {
const isBlack = isDark(color);
if (isBlack) {
return options?.dark ?? 'black';
}
return options?.light ?? 'white';
},
[isDark, options?.dark, options?.light],
);
}

View File

@@ -33,7 +33,7 @@ app
// Set up the proxy.
if (dev && devProxy) {
Object.keys(devProxy).forEach(function (context) {
Object.keys(devProxy).forEach(context => {
server.use(proxyMiddleware(context, devProxy[context]));
});
}

View File

@@ -7,11 +7,11 @@
"private": true,
"scripts": {
"lint": "eslint . --ext .ts --ext .tsx",
"dev": "node nextdev",
"dev": "export NODE_OPTIONS=--openssl-legacy-provider; node nextdev",
"start": "next start",
"typecheck": "tsc --noEmit",
"format": "prettier --config ./.prettierrc -c -w .",
"build": "next build && next export -o ../hyperglass/static/ui",
"build": "export NODE_OPTIONS=--openssl-legacy-provider; next build && next export -o ../hyperglass/static/ui",
"test": "jest"
},
"browserslist": "> 0.25%, not dead",
@@ -27,6 +27,7 @@
"dayjs": "^1.10.4",
"framer-motion": "^4.1.17",
"lodash": "^4.17.21",
"merge-anything": "^4.0.1",
"next": "^11.1.2",
"palette-by-numbers": "^0.1.5",
"plur": "^4.0.0",
@@ -40,7 +41,7 @@
"react-hook-form": "^7.7.0",
"react-markdown": "^5.0.3",
"react-query": "^3.16.0",
"react-select": "^4.3.1",
"react-select": "^5.2.1",
"react-table": "^7.7.0",
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",
@@ -51,9 +52,9 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@types/dagre": "^0.7.44",
"@types/express": "^4.17.13",
"@types/node": "^14.14.41",
"@types/react": "^17.0.3",
"@types/react-select": "^4.0.15",
"@types/react-table": "^7.7.1",
"@types/string-format": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^4.31.0",

View File

@@ -1,18 +1,19 @@
type AnyOption = {
interface AnyOption {
label: string;
};
}
export type SingleOption = AnyOption & {
export interface SingleOption<T extends Record<string, unknown> = Record<string, unknown>>
extends AnyOption {
value: string;
group?: string;
tags?: string[];
data?: Record<string, unknown>;
};
data?: T;
}
export type OptionGroup = AnyOption & {
options: SingleOption[];
};
export interface OptionGroup<Opt extends SingleOption> extends AnyOption {
options: Opt[];
}
export type SelectOption<T extends unknown = unknown> = (SingleOption | OptionGroup) & { data: T };
export type OptionsOrGroup<Opt extends SingleOption> = Array<Opt | OptionGroup<Opt>>;
export type OnChangeArgs = { field: string; value: string | string[] };

View File

@@ -61,3 +61,11 @@ declare global {
}
}
}
declare module 'react' {
// Enable generic typing with forwardRef.
// eslint-disable-next-line @typescript-eslint/ban-types
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}

View File

@@ -1666,6 +1666,21 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/body-parser@*":
version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/d3-array@*":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
@@ -1891,6 +1906,25 @@
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/express-serve-static-core@^4.17.18":
version "4.17.25"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.25.tgz#e42f7046adc65ece2eb6059b77aecfbe9e9f82e0"
integrity sha512-OUJIVfRMFijZukGGwTpKNFprqCCXk5WjNGvUgB/CxxBR40QWSjsNK86+yvGKlCOGc7sbwfHLaXhkG+NsytwBaQ==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.13":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.18"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/geojson@*":
version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
@@ -1972,6 +2006,11 @@
dependencies:
"@types/unist" "*"
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/node@*", "@types/node@^14.14.41":
version "14.14.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
@@ -1992,12 +2031,15 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react-dom@*":
version "16.9.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
dependencies:
"@types/react" "*"
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/range-parser@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-redux@^7.1.16":
version "7.1.16"
@@ -2009,16 +2051,6 @@
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-select@^4.0.15":
version "4.0.15"
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-4.0.15.tgz#2e6a1cff22c4bbae6c95b8dbee5b5097c12eae54"
integrity sha512-GPyBFYGMVFCtF4eg9riodEco+s2mflR10Nd5csx69+bcdvX6Uo9H/jgrIqovBU9yxBppB9DS66OwD6xxgVqOYQ==
dependencies:
"@emotion/serialize" "^1.0.0"
"@types/react" "*"
"@types/react-dom" "*"
"@types/react-transition-group" "*"
"@types/react-table@^7.7.1":
version "7.7.1"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.1.tgz#cac73133fc185e152e31435f8e6fce89ab868661"
@@ -2026,10 +2058,10 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@*":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
"@types/react-transition-group@^4.4.0":
version "4.4.4"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
dependencies:
"@types/react" "*"
@@ -2055,6 +2087,14 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
"@types/serve-static@*":
version "1.13.10"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
"@types/stack-utils@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@@ -4982,6 +5022,11 @@ is-typedarray@^1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-what@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -5862,6 +5907,14 @@ memoize-one@^5.0.0:
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
merge-anything@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-4.0.1.tgz#5c837cfa7adbb65fa5a4df178b37312493cb3609"
integrity sha512-KsFjBYc3juDoHz9Vzd5fte1nqp06H8SQ+yU344Dd0ZunwSgtltnC0kgKds8cbocJGyViLcBQuHkitbDXAqW+LQ==
dependencies:
is-what "^3.14.1"
ts-toolbelt "^9.3.12"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6704,7 +6757,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -6906,13 +6959,6 @@ react-hook-form@^7.7.0:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.7.0.tgz#11072091fde39775ad834321d9f18f160d47e997"
integrity sha512-WhTl6lbQrV942yzmDL+Eq9AGwG0gARHBH198wuxYIoxtvrsBt5EskdTcRjAYXvJv9N5ojd3t+QoT4QXgDi5l0g==
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==
dependencies:
prop-types "^15.5.8"
react-is@17.0.2, "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
@@ -6989,17 +7035,17 @@ react-remove-scroll@2.4.1:
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-select@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81"
integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==
react-select@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.2.1.tgz#416c25c6b79b94687702374e019c4f2ed9d159d6"
integrity sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==
dependencies:
"@babel/runtime" "^7.12.0"
"@emotion/cache" "^11.4.0"
"@emotion/react" "^11.1.1"
"@types/react-transition-group" "^4.4.0"
memoize-one "^5.0.0"
prop-types "^15.6.0"
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0"
react-shallow-renderer@^16.13.1:
@@ -7872,6 +7918,11 @@ ts-pnp@^1.1.6:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
ts-toolbelt@^9.3.12:
version "9.6.0"
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5"
integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==
tsconfig-paths@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"