mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
contract ABI filters
This commit is contained in:
+1
-1
@@ -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',
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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 }/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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',
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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 }
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
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 }
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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>
|
||||
|
||||
+18
-1
@@ -1,6 +1,6 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-2
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user