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

View file

@ -2,40 +2,44 @@ import { useMemo } from 'react';
import { Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react'; import { Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react';
import { components } from 'react-select'; import { components } from 'react-select';
import { If, Select } from '~/components'; import { If, Select } from '~/components';
import { isSingleValue } from '~/components/select';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import { useDirective, useFormState } from '~/hooks'; import { useDirective, useFormState } from '~/hooks';
import { isSelectDirective } from '~/types'; import { isSelectDirective } from '~/types';
import { UserIP } from './userIP'; 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 { Directive, SingleOption } from '~/types';
import type { TQueryTarget } 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)) { if (directive !== null && isSelectDirective(directive)) {
return directive.options.map(o => ({ return directive.options.map(o => ({
value: o.value, value: o.value,
label: o.name, label: o.name,
description: o.description, data: { description: o.description },
})); }));
} }
return []; return [];
} }
const Option = (props: OptionProps<Dict, false>) => { const Option = (props: OptionProps<OptionWithDescription, false>) => {
const { label, data } = props; const { label, data } = props;
return ( return (
<components.Option {...props}> <components.Option<OptionWithDescription, false, GroupBase<OptionWithDescription>> {...props}>
<Text as="span">{label}</Text> <Text as="span">{label}</Text>
<br /> <br />
<Text fontSize="xs" as="span"> <Text fontSize="xs" as="span">
{data.description} {data.data?.description}
</Text> </Text>
</components.Option> </components.Option>
); );
}; };
export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => { export const QueryTarget = (props: TQueryTarget): JSX.Element => {
const { name, register, onChange, placeholder } = props; const { name, register, onChange, placeholder } = props;
const bg = useColorValue('white', 'whiteAlpha.100'); 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 }); onChange({ field: name, value: e.target.value });
} }
function handleSelectChange(e: SingleOption | SingleOption[]): void { const handleSelectChange: SelectOnChange<OptionWithDescription> = e => {
if (!Array.isArray(e) && e !== null) { if (isSingleValue(e)) {
onChange({ field: name, value: e.value }); onChange({ field: name, value: e.value });
setTarget({ display: e.value }); setTarget({ display: e.value });
} }
} };
return ( return (
<> <>
<input {...register('queryTarget')} hidden readOnly value={form.queryTarget} /> <input {...register('queryTarget')} hidden readOnly value={form.queryTarget} />
<If c={directive !== null && isSelectDirective(directive)}> <If c={directive !== null && isSelectDirective(directive)}>
<Select <Select<OptionWithDescription, false>
size="lg"
name={name} name={name}
options={options} options={options}
innerRef={register}
components={{ Option }} components={{ Option }}
onChange={handleSelectChange} 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 { useFormContext } from 'react-hook-form';
import { components } from 'react-select'; import { components } from 'react-select';
import { Select } from '~/components'; 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 { UseRadioProps } from '@chakra-ui/react';
import type { MenuListComponentProps } from 'react-select'; import type { MenuListProps } from 'react-select';
import type { SingleOption, OptionGroup, SelectOption } from '~/types'; import type { SingleOption, OptionGroup, OptionsOrGroup } from '~/types';
import type { TOptions } from '~/components/select'; import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField } from './types'; 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; return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
} }
type UserFilter = { type UserFilter = {
selected: string; selected: string;
setSelected(n: string): void; 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) => ({ const useFilter = create<UserFilter>((set, get) => ({
@ -28,10 +31,9 @@ const useFilter = create<UserFilter>((set, get) => ({
set(() => ({ selected: newValue })); set(() => ({ selected: newValue }));
}, },
filter(candidate, input): boolean { filter(candidate, input): boolean {
const { const { label, data } = candidate;
label, const group = data?.group ?? null;
data: { group },
} = candidate;
if (input && (label || group)) { if (input && (label || group)) {
const search = input.toLowerCase(); const search = input.toLowerCase();
if (group) { if (group) {
@ -52,14 +54,14 @@ const useFilter = create<UserFilter>((set, get) => ({
function useOptions() { function useOptions() {
const filtered = useFormState(s => s.filtered); const filtered = useFormState(s => s.filtered);
return useMemo((): TOptions => { return useMemo((): OptionsOrGroup<QueryTypeOption> => {
const groupNames = new Set( const groupNames = new Set(
filtered.types filtered.types
.filter(t => t.groups.length > 0) .filter(t => t.groups.length > 0)
.map(t => t.groups) .map(t => t.groups)
.flat(), .flat(),
); );
const optGroups: OptionGroup[] = Array.from(groupNames).map(group => ({ const optGroups: OptionGroup<QueryTypeOption>[] = Array.from(groupNames).map(group => ({
label: group, label: group,
options: filtered.types options: filtered.types
.filter(t => t.groups.includes(group)) .filter(t => t.groups.includes(group))
@ -67,7 +69,7 @@ function useOptions() {
.sort(sorter), .sort(sorter),
})); }));
const noGroups: OptionGroup = { const noGroups: OptionGroup<QueryTypeOption> = {
label: '', label: '',
options: filtered.types options: filtered.types
.filter(t => t.groups.length === 0) .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 { children, ...rest } = props;
const filtered = useFormState(s => s.filtered); const filtered = useFormState(s => s.filtered);
const selected = useFilter(state => state.selected); const selected = useFilter(state => state.selected);
@ -150,27 +152,25 @@ export const QueryType = (props: TQuerySelectField): JSX.Element => {
formState: { errors }, formState: { errors },
} = useFormContext(); } = useFormContext();
const setSelection = useFormState(s => s.setSelection); const setSelection = useFormState(s => s.setSelection);
const selections = useFormState(s => s.selections); const selections = useFormSelections<QueryTypeOption>();
const setFormValue = useFormState(s => s.setFormValue); const setFormValue = useFormState(s => s.setFormValue);
const options = useOptions(); const options = useOptions();
const { filter } = useFilter(); // Intentionally re-render on any changes const { filter } = useFilter(); // Intentionally re-render on any changes
function handleChange(e: SingleOption | SingleOption[]): void { const handleChange: SelectOnChange<QueryTypeOption> = e => {
let value = ''; let value = '';
if (!Array.isArray(e) && e !== null) { if (isSingleValue(e)) {
// setFormValue('queryType', e.value); setSelection<QueryTypeOption>('queryType', e);
setSelection('queryType', e);
value = e.value; value = e.value;
} else { } else {
setFormValue('queryType', ''); setFormValue('queryType', '');
setSelection('queryType', null); setSelection<QueryTypeOption>('queryType', null);
} }
onChange({ field: 'queryType', value }); onChange({ field: 'queryType', value });
} };
return ( return (
<Select <Select<QueryTypeOption>
size="lg"
name="queryType" name="queryType"
options={options} options={options}
aria-label={label} aria-label={label}

View file

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

View file

@ -1,2 +1,3 @@
export * from './select'; 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 { 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 { label, data } = props;
const tags = Array.isArray(data.tags) ? (data.tags as string[]) : []; const tags = Array.isArray(data.tags) ? (data.tags as string[]) : [];
return ( return (
<components.Option {...props}> <components.Option<Opt, IsMulti, GroupBase<Opt>> {...props}>
<Box as="span" d={{ base: 'block', lg: 'inline' }}> <chakra.span d={{ base: 'block', lg: 'inline' }}>{label}</chakra.span>
{label}
</Box>
{tags.length > 0 && ( {tags.length > 0 && (
<HStack d={{ base: 'flex', lg: 'inline-flex' }} ms={{ base: 0, lg: 2 }} alignItems="center"> <HStack d={{ base: 'flex', lg: 'inline-flex' }} ms={{ base: 0, lg: 2 }} alignItems="center">
{tags.map(tag => ( {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 ReactSelect from 'react-select';
import { chakra, useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { useColorMode } from '~/context'; import { useColorMode } from '~/context';
import { Option } from './option'; import { Option } from './option';
import { import {
@ -17,67 +17,81 @@ import {
useMultiValueRemoveStyle, useMultiValueRemoveStyle,
useIndicatorSeparatorStyle, useIndicatorSeparatorStyle,
} from './styles'; } from './styles';
import { isSingleValue } from './types';
import type {
Props as ReactSelectProps,
MultiValue,
OnChangeValue,
SelectInstance,
} from 'react-select';
import type { SingleOption } from '~/types'; import type { SingleOption } from '~/types';
import type { TSelectBase, TSelectContext, TReactSelectChakra } from './types'; import type { TSelectBase, TSelectContext } from './types';
const SelectContext = createContext<TSelectContext>({} as TSelectContext); const SelectContext = createContext<TSelectContext>({} as TSelectContext);
export const useSelectContext = (): TSelectContext => useContext(SelectContext); 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 { isOpen, onOpen, onClose } = useDisclosure();
const { options, multi, onSelect, isError = false, components, ...rest } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>( const defaultOnChange: ReactSelectProps<Opt, IsMulti>['onChange'] = changed => {
() => ({ colorMode, isOpen, isError }), if (isSingleValue<Opt>(changed)) {
[colorMode, isError, isOpen], changed = [changed] as unknown as OnChangeValue<Opt, IsMulti>;
); }
if (typeof onSelect === 'function') {
onSelect(changed as MultiValue<Opt>);
}
};
const defaultOnChange = (changed: SingleOption | SingleOption[]) => { const menu = useMenuStyle<Opt, IsMulti>({ colorMode });
if (!Array.isArray(changed)) { const menuList = useMenuListStyle<Opt, IsMulti>({ colorMode });
changed = [changed]; const control = useControlStyle<Opt, IsMulti>({ colorMode });
} const option = useOptionStyle<Opt, IsMulti>({ colorMode });
if (typeof onSelect === 'function') { const singleValue = useSingleValueStyle<Opt, IsMulti>({ colorMode });
onSelect(changed); 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 }); return (
const multiValueLabel = useMultiValueLabelStyle({ colorMode }); <SelectContext.Provider value={{ colorMode, isOpen, isError }}>
const multiValueRemove = useMultiValueRemoveStyle({ colorMode }); <ReactSelect<Opt, IsMulti>
const menuPortal = useMenuPortal(); onChange={defaultOnChange}
const rsTheme = useRSTheme(); onMenuClose={onClose}
onMenuOpen={onOpen}
return ( isClearable={true}
<SelectContext.Provider value={selectContext}> options={options}
<ReactSelectChakra isMulti={isMulti}
onChange={defaultOnChange} theme={rsTheme}
onMenuClose={onClose} components={{ Option, ...components }}
onMenuOpen={onOpen} ref={ref}
isClearable={true} styles={{
options={options} menu,
isMulti={multi} option,
theme={rsTheme} control,
components={{ Option, ...components }} menuList,
styles={{ menuPortal,
menuPortal, multiValue,
multiValue, singleValue,
multiValueLabel, placeholder,
multiValueRemove, multiValueLabel,
menu: useMenuStyle, multiValueRemove,
option: useOptionStyle, indicatorSeparator,
control: useControlStyle, }}
menuList: useMenuListStyle, {...rest}
singleValue: useSingleValueStyle, />
placeholder: usePlaceholderStyle, </SelectContext.Provider>
indicatorSeparator: useIndicatorSeparatorStyle, );
}} },
{...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 */ import * as ReactSelect from 'react-select';
/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */
import type { import type { StylesProps, StylesConfigFunction } from 'react-select/dist/declarations/src/styles';
Props as IReactSelect, import type { Theme, SingleOption } from '~/types';
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';
export interface TSelectState { export type SelectOnChange<
[k: string]: string[]; Opt extends SingleOption = SingleOption,
} IsMulti extends boolean = boolean,
> = NonNullable<ReactSelect.Props<Opt, IsMulti>['onChange']>;
export type TOptions = Array<SingleOption | OptionGroup>; export interface TSelectBase<Opt extends SingleOption, IsMulti extends boolean>
extends ReactSelect.Props<Opt, IsMulti> {
export type TReactSelectChakra = Omit<IReactSelect, 'isMulti' | 'onSelect' | 'onChange'> &
Omit<BoxProps, 'onChange' | 'onSelect'>;
export interface TSelectBase extends TReactSelectChakra {
name: string; name: string;
multi?: boolean; isMulti?: IsMulti;
isError?: boolean; isError?: boolean;
options: TOptions;
required?: boolean; required?: boolean;
onSelect?: (s: SingleOption[]) => void; onSelect?: (s: ReactSelect.MultiValue<Opt>) => void;
onChange?: (c: SingleOption | SingleOption[]) => void;
colorScheme?: Theme.ColorNames; colorScheme?: Theme.ColorNames;
} }
@ -42,40 +24,32 @@ export interface TSelectContext {
isError: boolean; isError: boolean;
} }
export interface TMultiValueRemoveProps { export interface RSStyleCallbackProps {
children: Node; colorMode: 'light' | 'dark';
data: any;
innerProps: {
className: string;
onTouchEnd: (e: any) => void;
onClick: (e: any) => void;
onMouseDown: (e: any) => void;
};
selectProps: any;
} }
export interface TRSTheme extends Omit<RSTheme, 'borderRadius'> { type StyleConfigKeys = keyof ReactSelect.StylesConfig<
borderRadius: string | number; 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 function isMultiValue<Opt extends SingleOption>(
value: ReactSelect.SingleValue<Opt> | ReactSelect.MultiValue<Opt>,
export type TMenu = MenuProps<TOptions, false>; ): value is NonNullable<ReactSelect.MultiValue<Opt>> {
return value !== null && Array.isArray(value);
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>;

View file

@ -5,8 +5,9 @@ import plur from 'plur';
import isEqual from 'react-fast-compare'; import isEqual from 'react-fast-compare';
import { all, andJoin, dedupObjectArray, withDev } from '~/util'; import { all, andJoin, dedupObjectArray, withDev } from '~/util';
import type { SingleValue, MultiValue } from 'react-select';
import type { StateCreator } from 'zustand'; 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 { SingleOption, Directive, FormData, Text } from '~/types';
import type { UseDevice } from './types'; import type { UseDevice } from './types';
@ -21,9 +22,9 @@ interface FormValues {
/** /**
* Selected *options*, vs. values. * Selected *options*, vs. values.
*/ */
interface FormSelections { interface FormSelections<Opt extends SingleOption = SingleOption> {
queryLocation: SingleOption[]; queryLocation: MultiValue<Opt>;
queryType: SingleOption | null; queryType: SingleValue<Opt>;
} }
interface Filtered { interface Filtered {
@ -39,13 +40,13 @@ interface Target {
display: string; display: string;
} }
interface FormStateType { interface FormStateType<Opt extends SingleOption = SingleOption> {
// Values // Values
filtered: Filtered; filtered: Filtered;
form: FormValues; form: FormValues;
loading: boolean; loading: boolean;
responses: Responses; responses: Responses;
selections: FormSelections; selections: FormSelections<Opt>;
status: FormStatus; status: FormStatus;
target: Target; target: Target;
resolvedIsOpen: boolean; resolvedIsOpen: boolean;
@ -57,7 +58,13 @@ interface FormStateType {
addResponse(deviceId: string, data: QueryResponse): void; addResponse(deviceId: string, data: QueryResponse): void;
setLoading(value: boolean): void; setLoading(value: boolean): void;
setStatus(value: FormStatus): 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; setTarget(update: Partial<Target>): void;
getDirective(): Directive | null; getDirective(): Directive | null;
reset(): void; reset(): void;
@ -95,7 +102,10 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
set({ status }); 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 } })); set(state => ({ selections: { ...state.selections, [field]: value } }));
}, },
@ -223,6 +233,10 @@ export const useFormState = create<FormStateType>(
withDev<FormStateType>(formState, 'useFormState'), 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 { export function useView(): FormStatus {
const { status, form } = useFormState(({ status, form }) => ({ status, form })); const { status, form } = useFormState(({ status, form }) => ({ status, form }));
return useMemo(() => { 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 { getColor, isLight } from '@chakra-ui/theme-tools';
import { useTheme } from '~/context'; import { useTheme } from '~/context';
import type { TOpposingOptions } from './types'; 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 * 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. * opposing color should be black or white.
*/ */
export function useIsDark(color: string): boolean { export function useIsDark(color: string): boolean {
const isDarkFn = useIsDarkCallback();
return useMemo((): boolean => isDarkFn(color), [color, isDarkFn]);
}
export function useIsDarkCallback(): UseIsDarkCallbackReturn {
const theme = useTheme(); const theme = useTheme();
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) { return useCallback(
color = getColor(theme, color, color); (color: string): boolean => {
} if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
let opposingShouldBeDark = true; color = getColor(theme, color, color);
try { }
opposingShouldBeDark = isLight(color)(theme); let opposingShouldBeDark = true;
} catch (err) { try {
console.error(err); opposingShouldBeDark = isLight(color)(theme);
} } catch (err) {
return opposingShouldBeDark; console.error(err);
}
return opposingShouldBeDark;
},
[theme],
);
} }
/** /**
@ -36,3 +48,17 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str
} }
}, [isBlack, options?.dark, options?.light]); }, [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. // Set up the proxy.
if (dev && devProxy) { if (dev && devProxy) {
Object.keys(devProxy).forEach(function (context) { Object.keys(devProxy).forEach(context => {
server.use(proxyMiddleware(context, devProxy[context])); server.use(proxyMiddleware(context, devProxy[context]));
}); });
} }

View file

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

View file

@ -1,18 +1,19 @@
type AnyOption = { interface AnyOption {
label: string; label: string;
}; }
export type SingleOption = AnyOption & { export interface SingleOption<T extends Record<string, unknown> = Record<string, unknown>>
extends AnyOption {
value: string; value: string;
group?: string; group?: string;
tags?: string[]; tags?: string[];
data?: Record<string, unknown>; data?: T;
}; }
export type OptionGroup = AnyOption & { export interface OptionGroup<Opt extends SingleOption> extends AnyOption {
options: SingleOption[]; 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[] }; 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: dependencies:
"@babel/types" "^7.3.0" "@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@*": "@types/d3-array@*":
version "2.9.0" version "2.9.0"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012" 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" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== 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@*": "@types/geojson@*":
version "7946.0.7" version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
@ -1972,6 +2006,11 @@
dependencies: dependencies:
"@types/unist" "*" "@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": "@types/node@*", "@types/node@^14.14.41":
version "14.14.41" version "14.14.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" 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" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react-dom@*": "@types/qs@*":
version "16.9.8" version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
dependencies:
"@types/react" "*" "@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": "@types/react-redux@^7.1.16":
version "7.1.16" version "7.1.16"
@ -2009,16 +2051,6 @@
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
redux "^4.0.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": "@types/react-table@^7.7.1":
version "7.7.1" version "7.7.1"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.1.tgz#cac73133fc185e152e31435f8e6fce89ab868661" resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.1.tgz#cac73133fc185e152e31435f8e6fce89ab868661"
@ -2026,10 +2058,10 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-transition-group@*": "@types/react-transition-group@^4.4.0":
version "4.4.0" version "4.4.4"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
@ -2055,6 +2087,14 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== 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": "@types/stack-utils@^2.0.0":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" 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" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 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: isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 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" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== 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: merge-descriptors@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 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" kleur "^3.0.3"
sisteransi "^1.0.5" 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" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 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" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.7.0.tgz#11072091fde39775ad834321d9f18f160d47e997"
integrity sha512-WhTl6lbQrV942yzmDL+Eq9AGwG0gARHBH198wuxYIoxtvrsBt5EskdTcRjAYXvJv9N5ojd3t+QoT4QXgDi5l0g== 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: 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" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" 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-callback-ref "^1.2.3"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-select@^4.3.1: react-select@^5.2.1:
version "4.3.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.2.1.tgz#416c25c6b79b94687702374e019c4f2ed9d159d6"
integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q== integrity sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==
dependencies: dependencies:
"@babel/runtime" "^7.12.0" "@babel/runtime" "^7.12.0"
"@emotion/cache" "^11.4.0" "@emotion/cache" "^11.4.0"
"@emotion/react" "^11.1.1" "@emotion/react" "^11.1.1"
"@types/react-transition-group" "^4.4.0"
memoize-one "^5.0.0" memoize-one "^5.0.0"
prop-types "^15.6.0" prop-types "^15.6.0"
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0" react-transition-group "^4.3.0"
react-shallow-renderer@^16.13.1: 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" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== 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: tsconfig-paths@^3.11.0:
version "3.11.0" version "3.11.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"