Commit 16185d52 authored by tom's avatar tom

implement async select

parent 0d73ad81
'use client'; 'use client';
import type { CollectionItem, ListCollection } from '@chakra-ui/react'; import type { CollectionItem, ListCollection } from '@chakra-ui/react';
import { Select as ChakraSelect, Portal, useSelectContext } from '@chakra-ui/react'; import { Select as ChakraSelect, createListCollection, Portal, useSelectContext } from '@chakra-ui/react';
import { useDebounce } from '@uidotdev/usehooks';
import * as React from 'react'; import * as React from 'react';
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import { CloseButton } from './close-button'; import { CloseButton } from './close-button';
...@@ -203,3 +205,61 @@ export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) ...@@ -203,3 +205,61 @@ export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref)
</SelectRoot> </SelectRoot>
); );
}); });
export interface SelectAsyncProps extends Omit<SelectRootProps, 'collection'> {
placeholder: string;
portalled?: boolean;
loading?: boolean;
loadOptions: (input: string, currentValue: Array<string>) => Promise<ListCollection<CollectionItem>>;
extraControls?: React.ReactNode;
}
export const SelectAsync = React.forwardRef<HTMLDivElement, SelectAsyncProps>((props, ref) => {
const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, ...rest } = props;
const [ collection, setCollection ] = React.useState<ListCollection<CollectionItem>>(createListCollection({ items: [] }));
const [ inputValue, setInputValue ] = React.useState('');
const [ value, setValue ] = React.useState<Array<string>>([]);
const debouncedInputValue = useDebounce(inputValue, 300);
React.useEffect(() => {
loadOptions(debouncedInputValue, value).then(setCollection);
}, [ debouncedInputValue, loadOptions, value ]);
const handleFilterChange = React.useCallback((value: string) => {
setInputValue(value);
}, [ ]);
const handleValueChange = React.useCallback(({ value, items }: { value: Array<string>; items: Array<CollectionItem> }) => {
setValue(value);
onValueChange?.({ value, items });
}, [ onValueChange ]);
return (
<SelectRoot
ref={ ref }
collection={ collection }
variant="outline"
onValueChange={ handleValueChange }
{ ...rest }
>
<SelectControl loading={ loading }>
<SelectValueText placeholder={ placeholder }/>
</SelectControl>
<SelectContent portalled={ portalled }>
<FilterInput
placeholder="Search"
initialValue={ inputValue }
onChange={ handleFilterChange }
/>
{ extraControls }
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
);
});
...@@ -15,13 +15,13 @@ const ContractDetailsVerificationButton = ({ isLoading, addressHash, isPartially ...@@ -15,13 +15,13 @@ const ContractDetailsVerificationButton = ({ isLoading, addressHash, isPartially
return ( return (
<Link <Link
href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) } href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) }
mr={ isPartiallyVerified ? 0 : 3 }
ml={ isPartiallyVerified ? 0 : 'auto' }
flexShrink={ 0 }
asChild asChild
> >
<Button <Button
size="sm" size="sm"
mr={ isPartiallyVerified ? 0 : 3 }
ml={ isPartiallyVerified ? 0 : 'auto' }
flexShrink={ 0 }
loadingSkeleton={ isLoading } loadingSkeleton={ isLoading }
> >
Verify & publish Verify & publish
......
...@@ -8,7 +8,7 @@ import type { SmartContractVerificationConfig } from 'types/client/contract'; ...@@ -8,7 +8,7 @@ import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import { Checkbox } from 'toolkit/chakra/checkbox'; import { Checkbox } from 'toolkit/chakra/checkbox';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect'; import FormFieldSelectAsync from 'ui/shared/forms/fields/FormFieldSelectAsync';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -36,57 +36,55 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => { ...@@ -36,57 +36,55 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => {
}); });
}, [ getValues, resetField ]); }, [ getValues, resetField ]);
const options = React.useMemo(() => { const versions = React.useMemo(() => {
const versions = (() => { if (isStylus) {
if (isStylus) { return config?.stylus_compiler_versions;
return config?.stylus_compiler_versions; }
} if (isVyper) {
if (isVyper) { return config?.vyper_compiler_versions;
return config?.vyper_compiler_versions; }
} return config?.solidity_compiler_versions;
return config?.solidity_compiler_versions;
})();
return versions?.map((option) => ({ label: option, value: option })) || [];
}, [ isStylus, isVyper, config?.solidity_compiler_versions, config?.stylus_compiler_versions, config?.vyper_compiler_versions ]); }, [ isStylus, isVyper, config?.solidity_compiler_versions, config?.stylus_compiler_versions, config?.vyper_compiler_versions ]);
// const loadOptions = React.useCallback(async(inputValue: string) => { const loadOptions = React.useCallback(async(inputValue: string, currentValue: Array<string>) => {
// return options const items = versions
// .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase())) ?.filter((value) => !inputValue || currentValue.includes(value) || value.toLowerCase().includes(inputValue.toLowerCase()))
// .filter(({ label }) => isNightly ? true : !label.includes('nightly')) .filter((value) => isNightly ? true : !value.includes('nightly'))
// .slice(0, OPTIONS_LIMIT); .sort((a, b) => {
// }, [ isNightly, options ]); if (currentValue.includes(a)) {
return -1;
// TODO @tom2drum implement filtering the options }
const collection = React.useMemo(() => { if (currentValue.includes(b)) {
const items = options return 1;
// .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase())) }
.filter(({ label }) => isNightly ? true : !label.includes('nightly')) return 0;
.slice(0, OPTIONS_LIMIT); })
.slice(0, OPTIONS_LIMIT)
.map((value) => ({ label: value, value })) ?? [];
return createListCollection({ items }); return createListCollection({ items });
}, [ isNightly, options ]); }, [ isNightly, versions ]);
const extraControls = !isVyper && !isStylus ? (
<Checkbox
mb={ 2 }
checked={ isNightly }
onCheckedChange={ handleCheckboxChange }
disabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) : null;
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<> <FormFieldSelectAsync<FormFields, 'compiler'>
{ !isVyper && !isStylus && ( name="compiler"
<Checkbox placeholder="Compiler (enter version or use the dropdown)"
mb={ 2 } loadOptions={ loadOptions }
checked={ isNightly } extraControls={ extraControls }
onCheckedChange={ handleCheckboxChange } required
disabled={ formState.isSubmitting } />
>
Include nightly builds
</Checkbox>
) }
<FormFieldSelect<FormFields, 'compiler'>
name="compiler"
placeholder="Compiler (enter version or use the dropdown)"
collection={ collection }
required
/>
</>
{ isVyper || isStylus ? null : ( { isVyper || isStylus ? null : (
<chakra.div mt={{ base: 0, lg: 8 }}> <chakra.div mt={{ base: 0, lg: 8 }}>
<span >The compiler version is specified in </span> <span >The compiler version is specified in </span>
......
...@@ -7,7 +7,7 @@ import type { SmartContractVerificationConfig } from 'types/client/contract'; ...@@ -7,7 +7,7 @@ import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect'; import FormFieldSelectAsync from 'ui/shared/forms/fields/FormFieldSelectAsync';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -17,26 +17,34 @@ const ContractVerificationFieldZkCompiler = () => { ...@@ -17,26 +17,34 @@ const ContractVerificationFieldZkCompiler = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => ( const versions = React.useMemo(() => (
config?.zk_compiler_versions?.map((option) => ({ label: option, value: option })) || [] config?.zk_compiler_versions || []
), [ config?.zk_compiler_versions ]); ), [ config?.zk_compiler_versions ]);
// TODO @tom2drum implement filtering the options const loadOptions = React.useCallback(async(inputValue: string, currentValue: Array<string>) => {
const items = versions
const collection = React.useMemo(() => { ?.filter((value) => !inputValue || currentValue.includes(value) || value.toLowerCase().includes(inputValue.toLowerCase()))
const items = options .sort((a, b) => {
// .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase())) if (currentValue.includes(a)) {
.slice(0, OPTIONS_LIMIT); return -1;
}
if (currentValue.includes(b)) {
return 1;
}
return 0;
})
.slice(0, OPTIONS_LIMIT)
.map((value) => ({ label: value, value })) ?? [];
return createListCollection({ items }); return createListCollection({ items });
}, [ options ]); }, [ versions ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<FormFieldSelect<FormFields, 'zk_compiler'> <FormFieldSelectAsync<FormFields, 'zk_compiler'>
name="zk_compiler" name="zk_compiler"
placeholder="ZK compiler (enter version or use the dropdown)" placeholder="ZK compiler (enter version or use the dropdown)"
collection={ collection } loadOptions={ loadOptions }
required required
/> />
<Box> <Box>
......
import React from 'react';
import type { Path, FieldValues } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { SelectAsyncProps } from 'toolkit/chakra/select';
import { SelectAsync } from 'toolkit/chakra/select';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectAsyncProps;
const FormFieldSelectAsync = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
});
const isDisabled = formState.isSubmitting;
const handleChange = React.useCallback(({ value }: { value: Array<string> }) => {
field.onChange(value);
}, [ field ]);
const handleBlur = React.useCallback(() => {
field.onBlur();
}, [ field ]);
return (
<SelectAsync
ref={ field.ref }
name={ field.name }
value={ field.value }
onValueChange={ handleChange }
onInteractOutside={ handleBlur }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
{ ...rest }
/>
);
};
export default React.memo(FormFieldSelectAsync) as typeof FormFieldSelectAsync;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment