Commit 3a9f3d18 authored by tom's avatar tom

contract ABI filters

parent 27872bfe
......@@ -32,7 +32,7 @@ const RESTRICTED_MODULES = {
{
name: '@chakra-ui/react',
importNames: [
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'Link', 'Tag', 'Switch',
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'ButtonGroup', 'Link', 'Tag', 'Switch',
'Image', 'Popover', 'PopoverTrigger', 'PopoverContent', 'PopoverBody', 'PopoverFooter',
'DrawerRoot', 'DrawerBody', 'DrawerContent', 'DrawerOverlay', 'DrawerBackdrop', 'DrawerTrigger', 'Drawer',
'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription',
......
......@@ -8,6 +8,7 @@
"npm": "10.9.0"
},
"scripts": {
"postinstall": "yarn chakra:typegen",
"dev": "./tools/scripts/dev.sh",
"dev:preset": "./tools/scripts/dev.preset.sh",
"dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js",
......@@ -44,7 +45,7 @@
"@blockscout/bens-types": "1.4.1",
"@blockscout/stats-types": "2.0.0",
"@blockscout/visualizer-types": "0.2.0",
"@chakra-ui/react": "3.4.0",
"@chakra-ui/react": "3.8.0",
"@cloudnouns/kit": "1.1.6",
"@emotion/react": "11.14.0",
"@growthbook/growthbook-react": "0.21.0",
......
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
import type { ButtonProps as ChakraButtonProps, ButtonGroupProps as ChakraButtonGroupProps } from '@chakra-ui/react';
import {
AbsoluteCenter,
Button as ChakraButton,
ButtonGroup as ChakraButtonGroup,
Span,
Spinner,
} from '@chakra-ui/react';
import * as React from 'react';
import { Skeleton } from './skeleton';
interface ButtonLoadingProps {
loading?: boolean;
loadingText?: React.ReactNode;
......@@ -61,3 +64,61 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
);
},
);
export interface ButtonGroupProps extends ChakraButtonGroupProps {}
export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
function ButtonGroup(props, ref) {
const { ...rest } = props;
return (
<ChakraButtonGroup ref={ ref } { ...rest }/>
);
},
);
export interface ButtonGroupRadioProps extends Omit<ChakraButtonGroupProps, 'children' | 'onChange'> {
children: Array<React.ReactElement<ButtonProps>>;
onChange?: (value: string) => void;
defaultValue?: string;
loading?: boolean;
}
export const ButtonGroupRadio = React.forwardRef<HTMLDivElement, ButtonGroupRadioProps>(
function ButtonGroupRadio(props, ref) {
const { children, onChange, variant = 'segmented', defaultValue, loading = false, ...rest } = props;
const firstChildValue = React.useMemo(() => {
const firstChild = Array.isArray(children) ? children[0] : undefined;
return typeof firstChild?.props.value === 'string' ? firstChild.props.value : undefined;
}, [ children ]);
const [ value, setValue ] = React.useState<string | undefined>(defaultValue ?? firstChildValue);
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const value = event.currentTarget.value;
setValue(value);
onChange?.(value);
}, [ onChange ]);
const clonedChildren = React.Children.map(children, (child: React.ReactElement<ButtonProps>) => {
return React.cloneElement(child, {
onClick: handleItemClick,
selected: value === child.props.value,
variant,
});
});
return (
<Skeleton loading={ loading }>
<ChakraButtonGroup
ref={ ref }
gap={ 0 }
{ ...rest }
>
{ clonedChildren }
</ChakraButtonGroup>
</Skeleton>
);
},
);
......@@ -140,7 +140,7 @@ export const SelectRoot = React.forwardRef<
<ChakraSelect.Root
{ ...props }
ref={ ref }
positioning={{ sameWidth: false, ...props.positioning }}
positioning={{ sameWidth: false, ...props.positioning, offset: { mainAxis: 4, ...props.positioning?.offset } }}
>
{ props.asChild ? (
props.children
......
......@@ -45,6 +45,15 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
DEFAULT: { value: { _light: '{colors.gray.300}', _dark: '{colors.gray.600}' } },
},
},
segmented: {
fg: {
DEFAULT: { value: { _light: '{colors.blue.600}', _dark: '{colors.blue.300}' } },
selected: { value: { _light: '{colors.blue.700}', _dark: '{colors.gray.50}' } },
},
border: {
DEFAULT: { value: { _light: '{colors.blue.50}', _dark: '{colors.gray.800}' } },
},
},
hero: {
bg: {
DEFAULT: {
......
......@@ -131,6 +131,36 @@ export const recipe = defineRecipe({
},
},
},
segmented: {
bg: 'transparent',
color: 'button.segmented.fg',
borderColor: 'button.segmented.border',
borderWidth: '2px',
borderStyle: 'solid',
borderRadius: 'none',
_hover: {
color: 'link.primary.hover',
},
_selected: {
bg: 'button.segmented.border',
color: 'button.segmented.fg.selected',
_hover: {
bg: 'button.segmented.border',
color: 'button.segmented.fg.selected',
},
},
_notFirst: {
borderLeftWidth: '0',
},
_first: {
borderTopLeftRadius: 'base',
borderBottomLeftRadius: 'base',
},
_last: {
borderTopRightRadius: 'base',
borderBottomRightRadius: 'base',
},
},
plain: {
bg: 'transparent',
color: 'inherit',
......
......@@ -29,12 +29,12 @@ export const recipe = defineSlotRecipe({
},
},
trigger: {
fontWeight: '600',
outline: '0',
minW: 'var(--tabs-height)',
height: 'var(--tabs-height)',
display: 'flex',
alignItems: 'center',
fontWeight: 'medium',
position: 'relative',
cursor: 'button',
gap: '2',
......@@ -130,7 +130,6 @@ export const recipe = defineSlotRecipe({
variant: {
solid: {
trigger: {
fontWeight: '600',
borderRadius: 'base',
color: 'tabs.solid.fg',
bg: 'transparent',
......@@ -157,7 +156,6 @@ export const recipe = defineSlotRecipe({
},
},
trigger: {
fontWeight: '500',
color: 'tabs.secondary.fg',
bg: 'transparent',
borderWidth: '2px',
......
......@@ -20,7 +20,7 @@ const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
}
return (
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" listProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
<RoutedTabs tabs={ tabs } variant="secondary" size="sm" listProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
);
};
......
import { chakra, Flex } from '@chakra-ui/react';
import { chakra, createListCollection, Flex } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkNewTab from 'ui/shared/links/LinkNewTab';
import Select from 'ui/shared/select/Select';
export interface Item {
address: string;
......@@ -25,19 +25,20 @@ interface Props {
const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect, items, isLoading, label }: Props) => {
const handleItemSelect = React.useCallback((value: string) => {
const nextOption = items.find(({ address }) => address === value);
const handleItemSelect = React.useCallback(({ value }: { value: Array<string> }) => {
const nextOption = items.find(({ address }) => address === value[0]);
if (nextOption) {
onItemSelect(nextOption);
}
}, [ items, onItemSelect ]);
const options = React.useMemo(() => {
return items.map(({ address, name }) => ({ label: name || address, value: address }));
const collection = React.useMemo(() => {
const options = items.map(({ address, name }) => ({ label: name || address, value: address }));
return createListCollection({ items: options });
}, [ items ]);
if (isLoading) {
return <Skeleton h={ 6 } w={{ base: '300px', lg: '500px' }} className={ className }/>;
return <Skeleton loading h={ 6 } w={{ base: '300px', lg: '500px' }} className={ className }/>;
}
if (items.length === 0) {
......@@ -58,15 +59,23 @@ const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect,
return (
<Flex columnGap={ 3 } rowGap={ 2 } alignItems="center" className={ className }>
<chakra.span fontWeight={ 500 } fontSize="sm">{ label }</chakra.span>
<Select
options={ options }
name="contract-source-address"
defaultValue={ selectedItem.address }
onChange={ handleItemSelect }
isLoading={ isLoading }
maxW={{ base: '180px', lg: 'none' }}
fontWeight={ 600 }
/>
<SelectRoot
collection={ collection }
variant="outline"
defaultValue={ [ selectedItem.address ] }
onValueChange={ handleItemSelect }
>
<SelectControl maxW={{ base: '180px', lg: 'none' }} loading={ isLoading }>
<SelectValueText placeholder="Select contract"/>
</SelectControl>
<SelectContent>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
<Flex columnGap={ 2 } alignItems="center">
<CopyToClipboard text={ selectedItem.address } ml={ 0 }/>
<LinkNewTab
......
import { Alert, Button, Flex } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWeb3Wallet from 'lib/web3/useWallet';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
interface Props {
......@@ -25,7 +27,7 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
onClick={ web3Wallet.connect }
size="sm"
variant="outline"
isLoading={ web3Wallet.isOpen }
loading={ web3Wallet.isOpen }
loadingText="Connect wallet"
>
Connect wallet
......@@ -52,7 +54,7 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
})();
return (
<Skeleton isLoaded={ !isLoading }>
<Skeleton loading={ isLoading }>
<Alert status={ web3Wallet.address ? 'success' : 'warning' }>
{ content }
</Alert>
......
import { Alert } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Alert } from 'toolkit/chakra/alert';
interface Props {
isLoading?: boolean;
}
const ContractCustomAbiAlert = ({ isLoading }: Props) => {
return (
<Skeleton isLoaded={ !isLoading }>
<Alert status="warning">
Note: Contract with custom ABI is only meant for debugging purpose and it is the user’s responsibility to ensure that the provided ABI
matches the contract, otherwise errors may occur or results returned may be incorrect.
Blockscout is not responsible for any losses that arise from the use of Read & Write contract.
</Alert>
</Skeleton>
<Alert status="warning" loading={ isLoading }>
Note: Contract with custom ABI is only meant for debugging purpose and it is the user’s responsibility to ensure that the provided ABI
matches the contract, otherwise errors may occur or results returned may be incorrect.
Blockscout is not responsible for any losses that arise from the use of Read & Write contract.
</Alert>
);
};
......
import { Button, Flex, useDisclosure } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -7,8 +7,10 @@ import type { SmartContract } from 'types/api/contract';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { Button } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import Skeleton from 'ui/shared/chakra/Skeleton';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import useIsAuth from 'ui/snippets/auth/useIsAuth';
......@@ -58,7 +60,7 @@ const ContractMethodsCustom = ({ isLoading: isLoadingProp }: Props) => {
const updateButton = React.useMemo(() => {
return (
<Skeleton isLoaded={ !isLoading } ml="auto" mr="3" borderRadius="base">
<Skeleton loading={ isLoading } ml="auto" mr="3" borderRadius="base">
<Button
size="sm"
variant="outline"
......@@ -91,19 +93,19 @@ const ContractMethodsCustom = ({ isLoading: isLoadingProp }: Props) => {
onChange={ filters.onChange }
isLoading={ isLoading }
/>
<ContractMethodsContainer isLoading={ isLoading } isEmpty={ abi.length === 0 } type={ filters.methodType }>
{ /* <ContractMethodsContainer isLoading={ isLoading } isEmpty={ abi.length === 0 } type={ filters.methodType }>
<ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash } visibleItems={ filters.visibleItems }/>
</ContractMethodsContainer>
</ContractMethodsContainer> */ }
</>
) : (
<>
<Skeleton isLoaded={ !isLoading }>
<Skeleton loading={ isLoading }>
Add custom ABIs for this contract and access when logged into your account. Helpful for debugging,
functional testing and contract interaction.
</Skeleton>
<AuthGuard onAuthSuccess={ modal.onOpen }>
{ ({ onClick }) => (
<Skeleton isLoaded={ !isLoading } w="fit-content">
<Skeleton loading={ isLoading } w="fit-content">
<Button
size="sm"
onClick={ onClick }
......
......@@ -3,8 +3,8 @@ import React from 'react';
import type { MethodType } from './types';
import { ButtonGroupRadio, Button } from 'toolkit/chakra/button';
import FilterInput from 'ui/shared/filters/FilterInput';
import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
import type { MethodsFilters } from './useMethodsFilters';
import { TYPE_FILTER_OPTIONS } from './utils';
......@@ -18,8 +18,8 @@ interface Props {
const ContractMethodsFilters = ({ defaultMethodType, defaultSearchTerm, onChange, isLoading }: Props) => {
const handleTypeChange = React.useCallback((value: MethodType) => {
onChange({ type: 'method_type', value });
const handleTypeChange = React.useCallback((value: string) => {
onChange({ type: 'method_type', value: value as MethodType });
}, [ onChange ]);
const handleSearchTermChange = React.useCallback((value: string) => {
......@@ -28,21 +28,25 @@ const ContractMethodsFilters = ({ defaultMethodType, defaultSearchTerm, onChange
return (
<Flex columnGap={ 3 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }}>
<RadioButtonGroup<MethodType>
name="contract-methods-filter"
<ButtonGroupRadio
defaultValue={ defaultMethodType }
options={ TYPE_FILTER_OPTIONS }
onChange={ handleTypeChange }
w={{ lg: 'fit-content' }}
isLoading={ isLoading }
/>
loading={ isLoading }
>
{ TYPE_FILTER_OPTIONS.map((option) => (
<Button key={ option.value } value={ option.value } size="sm" px={ 3 }>
{ option.title }
</Button>
)) }
</ButtonGroupRadio>
<FilterInput
initialValue={ defaultSearchTerm }
onChange={ handleSearchTermChange }
placeholder="Search by method name"
w={{ base: '100%', lg: '450px' }}
size="xs"
isLoading={ isLoading }
size="sm"
loading={ isLoading }
/>
</Flex>
);
......
......@@ -58,7 +58,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }:
isLoading={ isInitialLoading }
/>
</div>
<ContractMethodsContainer
{ /* <ContractMethodsContainer
key={ selectedItem.address }
isLoading={ isInitialLoading || contractQuery.isPending }
isEmpty={ abi.length === 0 }
......@@ -72,7 +72,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }:
visibleItems={ filters.visibleItems }
sourceAddress={ selectedItem.address }
/>
</ContractMethodsContainer>
</ContractMethodsContainer> */ }
</Flex>
);
};
......
......@@ -36,9 +36,9 @@ const ContractMethodsRegular = ({ abi, isLoading }: Props) => {
onChange={ filters.onChange }
isLoading={ isLoading }
/>
<ContractMethodsContainer isLoading={ isLoading } isEmpty={ formattedAbi.length === 0 } type={ filters.methodType }>
{ /* <ContractMethodsContainer isLoading={ isLoading } isEmpty={ formattedAbi.length === 0 } type={ filters.methodType }>
<ContractAbi abi={ formattedAbi } tab={ tab } addressHash={ addressHash } visibleItems={ filters.visibleItems }/>
</ContractMethodsContainer>
</ContractMethodsContainer> */ }
</Flex>
);
};
......
......@@ -80,21 +80,21 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
component: <ContractDetails mainContractQuery={ contractQuery } channel={ channel } addressHash={ data.hash }/>,
subTabs: CONTRACT_DETAILS_TAB_IDS as unknown as Array<string>,
},
// contractQuery.data?.abi && {
// id: [ 'read_write_contract' as const, 'read_contract' as const, 'write_contract' as const ],
// title: 'Read/Write contract',
// component: <ContractMethodsRegular abi={ contractQuery.data.abi } isLoading={ contractQuery.isPlaceholderData }/>,
// },
// verifiedImplementations.length > 0 && {
// id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ],
// title: 'Read/Write proxy',
// component: <ContractMethodsProxy implementations={ verifiedImplementations } isLoading={ contractQuery.isPlaceholderData }/>,
// },
// config.features.account.isEnabled && {
// id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ],
// title: 'Custom ABI',
// component: <ContractMethodsCustom isLoading={ contractQuery.isPlaceholderData }/>,
// },
contractQuery.data?.abi && {
id: [ 'read_write_contract' as const, 'read_contract' as const, 'write_contract' as const ],
title: 'Read/Write contract',
component: <ContractMethodsRegular abi={ contractQuery.data.abi } isLoading={ contractQuery.isPlaceholderData }/>,
},
verifiedImplementations.length > 0 && {
id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ],
title: 'Read/Write proxy',
component: <ContractMethodsProxy implementations={ verifiedImplementations } isLoading={ contractQuery.isPlaceholderData }/>,
},
config.features.account.isEnabled && {
id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ],
title: 'Custom ABI',
component: <ContractMethodsCustom isLoading={ contractQuery.isPlaceholderData }/>,
},
// hasMudTab && {
// id: 'mud_system' as const,
// title: 'MUD System',
......
import {
Box,
Button,
} from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form';
......@@ -13,6 +10,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import { Button } from 'toolkit/chakra/button';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
......@@ -23,7 +21,7 @@ export type FormData = CustomAbi | {
type Props = {
data: FormData;
onClose: () => void;
onOpenChange: ({ open }: { open: boolean }) => void;
onSuccess?: () => Promise<void>;
setAlertVisible: (isAlertVisible: boolean) => void;
};
......@@ -36,7 +34,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const CustomAbiForm: React.FC<Props> = ({ data, onOpenChange, onSuccess, setAlertVisible }) => {
const formApi = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
......@@ -82,7 +80,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisi
return [ response, ...(prevData || []) ];
});
await onSuccess?.();
onClose();
onOpenChange({ open: false });
},
onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
const errorMap = error.payload?.errors;
......@@ -110,15 +108,15 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisi
<FormFieldAddress<Inputs>
name="contract_address_hash"
placeholder="Smart contract address (0x...)"
isRequired
required
bgColor="dialog.bg"
isReadOnly={ Boolean(data && 'contract_address_hash' in data) }
readOnly={ Boolean(data && 'contract_address_hash' in data) }
mb={ 5 }
/>
<FormFieldText<Inputs>
name="name"
placeholder="Project name"
isRequired
required
rules={{
maxLength: NAME_MAX_LENGTH,
}}
......@@ -128,10 +126,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisi
<FormFieldText<Inputs>
name="abi"
placeholder="Custom ABI [{...}] (JSON format)"
isRequired
required
asComponent="Textarea"
bgColor="dialog.bg"
size="lg"
size="2xl"
minH="300px"
mb={ 8 }
/>
......@@ -139,8 +137,8 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisi
<Button
size="lg"
type="submit"
isDisabled={ !formApi.formState.isDirty }
isLoading={ isPending }
disabled={ !formApi.formState.isDirty }
loading={ isPending }
>
{ data && 'id' in data ? 'Save' : 'Create custom ABI' }
</Button>
......
......@@ -7,25 +7,25 @@ import FormModal from 'ui/shared/FormModal';
import CustomAbiForm, { type FormData } from './CustomAbiForm';
type Props = {
isOpen: boolean;
onClose: () => void;
open: boolean;
onOpenChange: ({ open }: { open: boolean }) => void;
onSuccess?: () => Promise<void>;
data: FormData;
};
const CustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data, onSuccess }) => {
const CustomAbiModal: React.FC<Props> = ({ open, onOpenChange, data, onSuccess }) => {
const title = data && 'id' in data ? 'Edit custom ABI' : 'New custom ABI';
const text = !(data && 'id' in data) ? 'Double check the ABI matches the contract to prevent errors or incorrect results.' : '';
const [ isAlertVisible, setAlertVisible ] = useState(false);
const renderForm = useCallback(() => {
return <CustomAbiForm data={ data } onClose={ onClose } onSuccess={ onSuccess } setAlertVisible={ setAlertVisible }/>;
}, [ data, onClose, onSuccess ]);
return <CustomAbiForm data={ data } onOpenChange={ onOpenChange } onSuccess={ onSuccess } setAlertVisible={ setAlertVisible }/>;
}, [ data, onOpenChange, onSuccess ]);
return (
<FormModal<CustomAbi>
isOpen={ isOpen }
onClose={ onClose }
open={ open }
onOpenChange={ onOpenChange }
title={ title }
text={ text }
renderForm={ renderForm }
......
import { chakra, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Input } from 'toolkit/chakra/input';
import { InputGroup } from 'toolkit/chakra/input-group';
import type { SkeletonProps } from 'toolkit/chakra/skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import ClearButton from 'ui/shared/ClearButton';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
interface Props extends Omit<SkeletonProps, 'onChange' | 'loading'> {
onChange?: (searchTerm: string) => void;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
loading?: boolean;
size?: 'sm' | 'md' | 'lg';
placeholder: string;
initialValue?: string;
isLoading?: boolean;
type?: React.HTMLInputTypeAttribute;
name?: string;
};
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading, type, name }: Props) => {
const FilterInput = ({ onChange, size = 'sm', placeholder, initialValue, type, name, loading = false, ...rest }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
......@@ -35,22 +35,25 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
inputRef?.current?.focus();
}, [ onChange ]);
const startElement = <IconSvg name="search" color={{ _light: 'blackAlpha.600', _dark: 'whiteAlpha.600' }} boxSize={ 4 }/>;
const endElement = filterQuery ? <ClearButton onClick={ handleFilterQueryClear }/> : null;
return (
<Skeleton
isLoaded={ !isLoading }
className={ className }
minW="250px"
borderRadius="base"
loading={ loading }
{ ...rest }
>
<InputGroup
size={ size }
startElement={ startElement }
startElementProps={{ px: 2 }}
startOffset="32px"
endElement={ endElement }
endElementProps={{ px: 0, w: '32px' }}
endOffset="32px"
>
<InputLeftElement
pointerEvents="none"
>
<IconSvg name="search" color={ iconColor } boxSize={ 4 }/>
</InputLeftElement>
<Input
ref={ inputRef }
size={ size }
......@@ -63,15 +66,9 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
type={ type }
name={ name }
/>
{ filterQuery ? (
<InputRightElement>
<ClearButton onClick={ handleFilterQueryClear }/>
</InputRightElement>
) : null }
</InputGroup>
</Skeleton>
);
};
export default chakra(FilterInput);
export default FilterInput;
......@@ -50,6 +50,7 @@ const FormFieldText = <
<Textarea
{ ...field }
autoComplete="off"
flexGrow={ 1 }
{ ...inputProps as TextareaProps }
onBlur={ handleBlur }
/>
......
import { chakra, IconButton, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Link } from 'toolkit/chakra/link';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from '../IconSvg';
interface Props {
......@@ -10,25 +14,21 @@ interface Props {
}
const LinkNewTab = ({ className, label, href }: Props) => {
const iconColor = useColorModeValue('gray.400', 'gray.500');
return (
<Tooltip label={ label }>
<Tooltip content={ label }>
<IconButton
asChild
aria-label={ label ?? 'Open link' }
icon={ <IconSvg name="open-link" boxSize={ 5 }/> }
w="20px"
h="20px"
color={ iconColor }
variant="simple"
display="inline-block"
flexShrink={ 0 }
as="a"
href={ href }
target="_blank"
color="link.secondary"
_hover={{ color: 'link.primary.hover' }}
className={ className }
borderRadius={ 0 }
/>
>
<Link href={ href } target="_blank">
<IconSvg name="open-link" boxSize={ 5 }/>
</Link>
</IconButton>
</Tooltip>
);
};
......
import { chakra, ButtonGroup, Button, Flex, useRadio, useRadioGroup } from '@chakra-ui/react';
import { chakra, Flex, useRadioGroup } from '@chakra-ui/react';
import type { ChakraProps, UseRadioProps } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Button, ButtonGroup } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
// TODO @tom2drum remove this component
type RadioItemProps = {
title: string;
icon?: IconName;
......@@ -20,10 +22,10 @@ type RadioItemProps = {
type RadioButtonProps = UseRadioProps & RadioItemProps;
const RadioButton = (props: RadioButtonProps) => {
const { getInputProps, getRadioProps } = useRadio(props);
// const { getInputProps, getRadioProps } = useRadio(props);
const input = getInputProps();
const checkbox = getRadioProps();
// const input = getInputProps();
// const checkbox = getRadioProps();
if (props.onlyIcon) {
return (
......@@ -33,9 +35,9 @@ const RadioButton = (props: RadioButtonProps) => {
variant="radio_group"
selected={ props.isChecked }
>
<input { ...input }/>
{ /* <input { ...input }/> */ }
<Flex
{ ...checkbox }
// { ...checkbox }
>
<IconSvg name={ props.icon } boxSize={ 5 }/>
</Flex>
......@@ -50,10 +52,10 @@ const RadioButton = (props: RadioButtonProps) => {
variant="radio_group"
selected={ props.isChecked }
>
<input { ...input }/>
{ /* <input { ...input }/> */ }
<Flex
alignItems="center"
{ ...checkbox }
// { ...checkbox }
>
{ props.title }
{ props.contentAfter }
......@@ -63,7 +65,7 @@ const RadioButton = (props: RadioButtonProps) => {
};
type RadioButtonGroupProps<T extends string> = {
onChange: (value: T) => void;
onChange: ({ value }: { value: T }) => void;
name: string;
defaultValue: string;
options: Array<{ value: T } & RadioItemProps>;
......@@ -73,14 +75,14 @@ type RadioButtonGroupProps<T extends string> = {
};
const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, options, autoWidth = false, className, isLoading }: RadioButtonGroupProps<T>) => {
const { getRootProps, getRadioProps } = useRadioGroup({ name, defaultValue, onChange });
const { getRootProps } = useRadioGroup({ name, defaultValue, onValueChange: onChange });
const group = getRootProps();
const root = getRootProps();
return (
<Skeleton isLoaded={ !isLoading }>
<Skeleton loading={ isLoading }>
<ButtonGroup
{ ...group }
{ ...root }
className={ className }
isAttached
size="sm"
......@@ -88,8 +90,8 @@ const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, opti
gridTemplateColumns={ `repeat(${ options.length }, ${ autoWidth ? 'auto' : '1fr' })` }
>
{ options.map((option) => {
const props = getRadioProps({ value: option.value });
return <RadioButton { ...props } key={ option.value } { ...option }/>;
// const props = getRadioProps({ value: option.value });
return <RadioButton key={ option.value } { ...option }/>;
}) }
</ButtonGroup>
</Skeleton>
......
import React from 'react';
import { Button } from 'toolkit/chakra/button';
import { Button, ButtonGroupRadio } from 'toolkit/chakra/button';
import { Checkbox } from 'toolkit/chakra/checkbox';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Link } from 'toolkit/chakra/link';
......@@ -216,7 +216,24 @@ const ButtonShowcase = () => {
</Link>
</Sample>
</SamplesStack>
<SectionSubHeader>Button Group Radio</SectionSubHeader>
<SamplesStack>
<Sample>
<ButtonGroupRadio>
<Button value="option-1">Option 1</Button>
<Button value="option-2">Option 2</Button>
<Button value="option-3">Option 3</Button>
</ButtonGroupRadio>
<ButtonGroupRadio loading>
<Button value="option-1">Option 1</Button>
<Button value="option-2">Option 2</Button>
<Button value="option-3">Option 3</Button>
</ButtonGroupRadio>
</Sample>
</SamplesStack>
</Section>
</Container>
);
};
......
......@@ -3,6 +3,7 @@ import React from 'react';
import { Field } from 'toolkit/chakra/field';
import { Input } from 'toolkit/chakra/input';
import { InputGroup } from 'toolkit/chakra/input-group';
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import { Section, Container, SectionHeader, SamplesStack, Sample, SectionSubHeader } from './parts';
......@@ -119,12 +120,23 @@ const InputShowcase = () => {
</Field>
</Sample>
<Sample label="with start element">
<InputGroup startElement={ <IconSvg name="search" boxSize={ 5 }/> }>
<Input placeholder="Search"/>
<InputGroup startElement={ <IconSvg name="collection" boxSize={ 5 }/> }>
<Input placeholder="Type in something"/>
</InputGroup>
</Sample>
</SamplesStack>
</Section>
<Section>
<SectionHeader>Examples</SectionHeader>
<SectionSubHeader>Filter input</SectionSubHeader>
<SamplesStack>
<Sample>
<FilterInput placeholder="Search by method name"/>
<FilterInput placeholder="Search by method name" loading/>
</Sample>
</SamplesStack>
</Section>
</Container>
);
};
......
This diff is collapsed.
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