Commit 17ea4d81 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #85 from blockscout/custom-abi

custom abi page
parents b96198c8 3950aae6
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import CustomAbi from 'ui/pages/CustomAbi';
const CustomAbiPage: NextPage = () => {
return (
<>
<Head><title>Custom ABI</title></Head>
<CustomAbi/>
</>
);
};
export default CustomAbiPage;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/custom_abis/${ req.query.id }`;
};
const customAbiHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default customAbiHandler;
import type { CustomAbis } from 'types/api/account';
import handler from 'lib/api/handler';
const customAbiHandler = handler<CustomAbis>(() => '/account/v1/user/custom_abis', [ 'GET', 'POST' ]);
export default customAbiHandler;
...@@ -6,21 +6,92 @@ import type { Dict } from '@chakra-ui/utils'; ...@@ -6,21 +6,92 @@ import type { Dict } from '@chakra-ui/utils';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
const activeInputStyles = { const getActiveLabelStyles = (theme: Dict, fc: string, bc: string, size: 'md' | 'lg') => {
paddingTop: '30px', const baseStyles = {
paddingBottom: '10px', backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
};
switch (size) {
case 'md': {
return {
...baseStyles,
padding: '10px 16px 2px 16px',
};
}
case 'lg': {
return {
...baseStyles,
padding: '16px 24px 2px 24px',
};
}
}
};
const getDefaultLabelStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
};
}
case 'lg': {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
};
}
}
};
const getPaddingX = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return '16px';
}
case 'lg': {
return '24px';
}
}
}; };
const getActiveLabelStyles = (theme: Dict, fc: string) => ({ const getActiveInputStyles = (size: 'md' | 'lg') => {
color: getColor(theme, fc), switch (size) {
transform: 'scale(0.75) translateY(-10px)', case 'md': {
}); return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
}
}
};
const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionProps) => { const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionProps) => {
const { theme } = props; const { theme, backgroundColor, size = 'md' } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props); const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
const activeLabelStyles = getActiveLabelStyles(theme, fc); const px = getPaddingX(size);
const activeInputStyles = getActiveInputStyles(size);
const activeLabelStyles = getActiveLabelStyles(theme, fc, bc, size);
return { return {
container: { container: {
...@@ -37,22 +108,19 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP ...@@ -37,22 +108,19 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP
}, },
// label's styles // label's styles
label: { label: {
left: '22px', ...getDefaultLabelStyles(size),
left: '2px',
top: '2px',
zIndex: 2, zIndex: 2,
position: 'absolute', position: 'absolute',
color: mode('gray.500', 'whiteAlpha.400')(props), borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent', backgroundColor: 'transparent',
pointerEvents: 'none', pointerEvents: 'none',
margin: 0, margin: 0,
transformOrigin: 'left top', transformOrigin: 'top left',
fontSize: 'md', transitionProperty: 'font-size, line-height, padding, top, background-color',
lineHeight: '20px',
},
'input + label': {
top: 'calc(50% - 10px);',
},
'textarea + label': {
top: '20px',
}, },
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': { 'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': {
...activeLabelStyles, ...activeLabelStyles,
...@@ -62,7 +130,10 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP ...@@ -62,7 +130,10 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP
}, },
// input's styles // input's styles
'input, textarea': { 'input, textarea': {
padding: '20px', padding: px,
},
'input[disabled] + label, textarea[disabled] + label': {
backgroundColor: 'transparent',
}, },
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': { 'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': {
...activeInputStyles, ...activeInputStyles,
......
...@@ -7,16 +7,17 @@ const baseStyleDialog: SystemStyleFunction = (props) => { ...@@ -7,16 +7,17 @@ const baseStyleDialog: SystemStyleFunction = (props) => {
return { return {
padding: 8, padding: 8,
borderRadius: 'lg', borderRadius: 'lg',
bg: mode('white', 'gray.800')(props), bg: mode('white', 'gray.900')(props),
}; };
}; };
const baseStyleHeader = { const baseStyleHeader: SystemStyleFunction = (props) => ({
padding: 0, padding: 0,
marginBottom: 8, marginBottom: 8,
fontSize: '2xl', fontSize: '2xl',
lineHeight: 10, lineHeight: 10,
}; color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
});
const baseStyleBody = { const baseStyleBody = {
padding: 0, padding: 0,
...@@ -34,7 +35,7 @@ const baseStyleCloseButton: SystemStyleFunction = (props) => { ...@@ -34,7 +35,7 @@ const baseStyleCloseButton: SystemStyleFunction = (props) => {
right: 8, right: 8,
height: 10, height: 10,
width: 10, width: 10,
color: mode('gray.700', 'gray.600')(props), color: mode('gray.700', 'gray.500')(props),
_hover: { color: 'blue.400' }, _hover: { color: 'blue.400' },
_active: { bg: 'none' }, _active: { bg: 'none' },
}; };
...@@ -45,7 +46,7 @@ const baseStyleOverlay = { ...@@ -45,7 +46,7 @@ const baseStyleOverlay = {
const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({ const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({
dialog: baseStyleDialog(props), dialog: baseStyleDialog(props),
header: baseStyleHeader, header: baseStyleHeader(props),
body: baseStyleBody, body: baseStyleBody,
footer: baseStyleFooter, footer: baseStyleFooter,
closeButton: baseStyleCloseButton(props), closeButton: baseStyleCloseButton(props),
......
...@@ -11,7 +11,7 @@ const variantSimple: PartsStyleFunction<typeof parts> = (props) => { ...@@ -11,7 +11,7 @@ const variantSimple: PartsStyleFunction<typeof parts> = (props) => {
return { return {
th: { th: {
border: 0, border: 0,
color: mode('gray.600', 'gray.50')(props), color: mode('gray.600', 'whiteAlpha.700')(props),
...transitionProps, ...transitionProps,
}, },
thead: { thead: {
......
...@@ -5,17 +5,18 @@ import getDefaultFormColors from './getDefaultFormColors'; ...@@ -5,17 +5,18 @@ import getDefaultFormColors from './getDefaultFormColors';
import getDefaultTransitionProps from './getDefaultTransitionProps'; import getDefaultTransitionProps from './getDefaultTransitionProps';
export default function getOutlinedFieldStyles(props: StyleFunctionProps) { export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
const { theme } = props; const { theme, borderColor } = props;
const { focusColor: fc, errorColor: ec, filledColor: flc } = getDefaultFormColors(props); const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const transitionProps = getDefaultTransitionProps(); const transitionProps = getDefaultTransitionProps();
return { return {
border: '2px solid', border: '2px solid',
bg: 'inherit', // filled input
borderColor: getColor(theme, flc), backgroundColor: 'transparent',
borderColor: mode('gray.300', 'gray.600')(props),
...transitionProps, ...transitionProps,
_hover: { _hover: {
borderColor: mode('gray.200', 'whiteAlpha.400')(props), borderColor: mode('gray.200', 'gray.500')(props),
}, },
_readOnly: { _readOnly: {
boxShadow: 'none !important', boxShadow: 'none !important',
...@@ -23,7 +24,7 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) { ...@@ -23,7 +24,7 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
}, },
_disabled: { _disabled: {
opacity: 1, opacity: 1,
background: mode('gray.200', 'whiteAlpha.400')(props), backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none', border: 'none',
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
...@@ -39,7 +40,8 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) { ...@@ -39,7 +40,8 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_placeholder: { _placeholder: {
color: mode('blackAlpha.600', 'whiteAlpha.600')(props), color: mode('blackAlpha.600', 'whiteAlpha.600')(props),
}, },
':placeholder-shown:not(:focus-visible):not(:hover)': { borderColor: mode('blackAlpha.100', 'whiteAlpha.200')(props) }, // not filled input
':placeholder-shown:not(:focus-visible):not(:hover)': { borderColor: borderColor || mode('gray.100', 'gray.700')(props) },
':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' },
......
...@@ -71,3 +71,27 @@ export interface WatchlistAddressNew { ...@@ -71,3 +71,27 @@ export interface WatchlistAddressNew {
} }
export type WatchlistAddresses = Array<WatchlistAddress> export type WatchlistAddresses = Array<WatchlistAddress>
export type CustomAbis = Array<CustomAbi>
export interface CustomAbi {
name: string;
id: number;
contract_address_hash: string;
abi: Array<AbiItem>;
}
export interface AbiItem {
type: 'function';
stateMutability: 'nonpayable' | 'view';
payable: boolean;
outputs: Array<AbiInputOutput>;
name: string;
inputs: Array<AbiInputOutput>;
constant: boolean;
}
interface AbiInputOutput {
type: 'uint256';
name: string;
}
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
...@@ -28,6 +29,7 @@ const NAME_MAX_LENGTH = 100; ...@@ -28,6 +29,7 @@ const NAME_MAX_LENGTH = 100;
const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => { const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
useEffect(() => { useEffect(() => {
setValue('token', data?.api_key || ''); setValue('token', data?.api_key || '');
...@@ -88,7 +90,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => { ...@@ -88,7 +90,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => {
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => { const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return ( return (
<FormControl variant="floating" id="name" isRequired> <FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(errors.name) } isInvalid={ Boolean(errors.name) }
...@@ -97,7 +99,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => { ...@@ -97,7 +99,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => {
<FormLabel>Application name for API key (e.g Web3 project)</FormLabel> <FormLabel>Application name for API key (e.g Web3 project)</FormLabel>
</FormControl> </FormControl>
); );
}, [ errors ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <>
......
import {
Box,
Button,
FormControl,
FormLabel,
Input,
Textarea,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect } from 'react';
import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis } from 'types/api/account';
type Props = {
data?: CustomAbi;
onClose: () => void;
}
type Inputs = {
contract_address_hash: string;
name: string;
abi: string;
}
// idk, maybe there is no limit
const NAME_MAX_LENGTH = 100;
const CustomAbiForm: React.FC<Props> = ({ data, onClose }) => {
const { control, formState: { errors }, setValue, handleSubmit } = useForm<Inputs>();
const queryClient = useQueryClient();
useEffect(() => {
setValue('contract_address_hash', data?.contract_address_hash || '');
setValue('name', data?.name || '');
setValue('abi', JSON.stringify(data?.abi) || '');
}, [ setValue, data ]);
const customAbiKey = (data: Inputs & { id?: number }) => {
const body = JSON.stringify({ name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi });
if (!data.id) {
return fetch('/api/account/custom-abis', { method: 'POST', body });
}
return fetch(`/api/account/custom-abis/${ data.id }`, { method: 'PUT', body });
};
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const mutation = useMutation(customAbiKey, {
onSuccess: async(data) => {
const response: CustomAbi = await data.json();
queryClient.setQueryData([ 'custom-abis' ], (prevData: CustomAbis | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) {
return prevData.map((item) => {
if (item.id === response.id) {
return response;
}
return item;
});
}
return [ ...(prevData || []), response ];
});
onClose();
},
// eslint-disable-next-line no-console
onError: console.error,
});
const onSubmit: SubmitHandler<Inputs> = useCallback((formData) => {
mutation.mutate({ ...formData, id: data?.id });
}, [ mutation, data ]);
const renderContractAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'contract_address_hash'>}) => {
return (
<FormControl variant="floating" id="contract_address_hash" isRequired backgroundColor={ formBackgroundColor }>
<Input
{ ...field }
isInvalid={ Boolean(errors.contract_address_hash) }
/>
<FormLabel>Smart contract address (0x...)</FormLabel>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }>
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>Project name</FormLabel>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => {
return (
<FormControl variant="floating" id="abi" isRequired backgroundColor={ formBackgroundColor }>
<Textarea
{ ...field }
size="lg"
isInvalid={ Boolean(errors.abi) }
/>
<FormLabel>{ `Custom ABI [{...}] (JSON format)` }</FormLabel>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
return (
<>
<Box>
<Controller
name="contract_address_hash"
control={ control }
render={ renderContractAddressInput }
/>
</Box>
<Box marginTop={ 5 }>
<Controller
name="name"
control={ control }
rules={{
maxLength: NAME_MAX_LENGTH,
}}
render={ renderNameInput }
/>
</Box>
<Box marginTop={ 5 }>
<Controller
name="abi"
control={ control }
render={ renderAbiInput }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
</Box>
</>
);
};
export default React.memo(CustomAbiForm);
import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account';
import FormModal from 'ui/shared/FormModal';
import CustomAbiForm from './CustomAbiForm';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: CustomAbi;
}
const CustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit custom ABI' : 'New custom ABI';
const text = 'Double check the ABI matches the contract to prevent errors or incorrect results.';
const renderForm = useCallback(() => {
return <CustomAbiForm data={ data } onClose={ onClose }/>;
}, [ data, onClose ]);
return (
<FormModal<CustomAbi>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
text={ text }
data={ data }
renderForm={ renderForm }
/>
);
};
export default React.memo(CustomAbiModal);
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
import type { CustomAbis, CustomAbi } from 'types/api/account';
import CustomAbiTableItem from './CustomAbiTableItem';
interface Props {
data: CustomAbis;
onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void;
}
const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>ABI for Smart contract address (0x...)</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<CustomAbiTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default React.memo(CustomAbiTable);
import {
Tr,
Td,
HStack,
Text,
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DeleteButton from 'ui/shared/DeleteButton';
import EditButton from 'ui/shared/EditButton';
interface Props {
item: CustomAbi;
onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void;
}
const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.id }>
<Td>
<HStack>
<Text fontSize="md" fontWeight={ 600 }>{ item.contract_address_hash }</Text>
<CopyToClipboard text={ item.contract_address_hash }/>
</HStack>
<Text fontSize="sm" marginTop={ 0.5 } variant="secondary">{ item.name }</Text>
</Td>
<Td>
<HStack spacing={ 6 }>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td>
</Tr>
);
};
export default React.memo(CustomAbiTableItem);
import { Text } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data: CustomAbi;
}
const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const deleteApiKey = () => {
return fetch(`/api/account/custom-abis/${ data.id }`, { method: 'DELETE' });
};
const mutation = useMutation(deleteApiKey, {
onSuccess: async() => {
queryClient.setQueryData([ 'custom-abis' ], (prevData: CustomAbis | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
onClose();
},
// eslint-disable-next-line no-console
onError: console.error,
});
const onDelete = useCallback(() => {
mutation.mutate(data);
}, [ data, mutation ]);
const renderText = useCallback(() => {
return (
<Text display="flex">Custom ABI for<Text fontWeight="600" whiteSpace="pre">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
);
}, [ data.name ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Remove custom ABI"
renderContent={ renderText }
pending={ mutation.isLoading }
/>
);
};
export default React.memo(DeleteCustomAbiModal);
...@@ -30,6 +30,7 @@ const SearchBar = () => { ...@@ -30,6 +30,7 @@ const SearchBar = () => {
placeholder="Search by addresses / transactions / block / token... " placeholder="Search by addresses / transactions / block / token... "
ml="1px" ml="1px"
onChange={ handleChange } onChange={ handleChange }
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
/> />
</InputGroup> </InputGroup>
</form> </form>
......
...@@ -19,7 +19,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => { ...@@ -19,7 +19,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => {
as={ networksIcon } as={ networksIcon }
width="16px" width="16px"
height="16px" height="16px"
color={ useColorModeValue('gray.500', 'white') } color={ useColorModeValue('gray.500', 'gray.400') }
_hover={{ color: 'blue.400' }} _hover={{ color: 'blue.400' }}
marginLeft={ isCollapsed ? '0px' : '27px' } marginLeft={ isCollapsed ? '0px' : '27px' }
cursor="pointer" cursor="pointer"
......
import { Box, Button, HStack, Text, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import Page from 'ui/shared/Page/Page';
import SkeletonTable from 'ui/shared/SkeletonTable';
const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError } = useQuery<unknown, unknown, CustomAbis>([ 'custom-abis' ], async() => {
const response = await fetch('/api/account/custom-abis');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data);
customAbiModalProps.onOpen();
}, [ customAbiModalProps ]);
const onCustomAbiModalClose = useCallback(() => {
setCustomAbiModalData(undefined);
customAbiModalProps.onClose();
}, [ customAbiModalProps ]);
const onDeleteClick = useCallback((data: CustomAbi) => {
setDeleteModalData(data);
deleteModalProps.onOpen();
}, [ deleteModalProps ]);
const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const content = (() => {
if (isLoading || isError) {
return (
<>
<SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
}
return (
<>
{ data.length > 0 && (
<CustomAbiTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
) }
<HStack marginTop={ 8 } spacing={ 5 }>
<Button
variant="primary"
size="lg"
onClick={ customAbiModalProps.onOpen }
>
Add custom ABI
</Button>
</HStack>
</>
);
})();
return (
<Page>
<Box h="100%">
<AccountPageHeader text="Custom ABI"/>
<Text marginBottom={ 12 }>
Add custom ABIs for any contract and access when logged into your account. Helpful for debugging, functional testing and contract interaction.
</Text>
{ content }
</Box>
<CustomAbiModal { ...customAbiModalProps } onClose={ onCustomAbiModalClose } data={ customAbiModalData }/>
{ deleteModalData && <DeleteCustomAbiModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</Page>
);
};
export default CustomAbiPage;
import { import {
Box, Box,
Button, Button,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
...@@ -28,6 +29,7 @@ type Inputs = { ...@@ -28,6 +29,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose }) => { const AddressForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
useEffect(() => { useEffect(() => {
setValue('address', data?.address_hash || ''); setValue('address', data?.address_hash || '');
...@@ -67,12 +69,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose }) => { ...@@ -67,12 +69,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose }) => {
}; };
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) }/>; return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>; return <TagInput field={ field } isInvalid={ Boolean(errors.tag) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <>
......
import { import {
Box, Box,
Button, Button,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
...@@ -28,6 +29,7 @@ type Inputs = { ...@@ -28,6 +29,7 @@ type Inputs = {
const TransactionForm: React.FC<Props> = ({ data, onClose }) => { const TransactionForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
useEffect(() => { useEffect(() => {
setValue('transaction', data?.transaction_hash || ''); setValue('transaction', data?.transaction_hash || '');
...@@ -68,12 +70,12 @@ const TransactionForm: React.FC<Props> = ({ data, onClose }) => { ...@@ -68,12 +70,12 @@ const TransactionForm: React.FC<Props> = ({ data, onClose }) => {
}; };
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => { const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) }/>; return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>; return <TagInput field={ field } isInvalid={ Boolean(errors.tag) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <>
......
...@@ -12,7 +12,7 @@ interface Props { ...@@ -12,7 +12,7 @@ interface Props {
export default function PublicTagFormComment({ control }: Props) { export default function PublicTagFormComment({ control }: Props) {
const renderComment = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'comment'>}) => { const renderComment = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'comment'>}) => {
return ( return (
<FormControl variant="floating" id={ field.name }> <FormControl variant="floating" id={ field.name } size="lg">
<Textarea <Textarea
{ ...field } { ...field }
size="lg" size="lg"
......
...@@ -13,7 +13,7 @@ interface Props<TInputs extends FieldValues> { ...@@ -13,7 +13,7 @@ interface Props<TInputs extends FieldValues> {
export default function PublicTagsFormInput<Inputs extends FieldValues>({ label, control, required, fieldName }: Props<Inputs>) { export default function PublicTagsFormInput<Inputs extends FieldValues>({ label, control, required, fieldName }: Props<Inputs>) {
const renderInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, typeof fieldName>}) => { const renderInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, typeof fieldName>}) => {
return ( return (
<FormControl variant="floating" id={ field.name } isRequired={ required }> <FormControl variant="floating" id={ field.name } isRequired={ required } size="lg">
<Input <Input
{ ...field } { ...field }
size="lg" size="lg"
......
...@@ -13,6 +13,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { ...@@ -13,6 +13,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
isInvalid: boolean; isInvalid: boolean;
size?: string; size?: string;
placeholder?: string; placeholder?: string;
backgroundColor?: string;
} }
export default function AddressInput<Inputs extends FieldValues, Name extends Path<Inputs>>( export default function AddressInput<Inputs extends FieldValues, Name extends Path<Inputs>>(
...@@ -21,9 +22,10 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa ...@@ -21,9 +22,10 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
isInvalid, isInvalid,
size, size,
placeholder = 'Address (0x...)', placeholder = 'Address (0x...)',
backgroundColor,
}: Props<Inputs, Name>) { }: Props<Inputs, Name>) {
return ( return (
<FormControl variant="floating" id="address" isRequired> <FormControl variant="floating" id="address" isRequired backgroundColor={ backgroundColor } size={ size }>
<Input <Input
{ ...field } { ...field }
isInvalid={ isInvalid } isInvalid={ isInvalid }
......
...@@ -11,11 +11,12 @@ const TAG_MAX_LENGTH = 35; ...@@ -11,11 +11,12 @@ const TAG_MAX_LENGTH = 35;
type Props<Field> = { type Props<Field> = {
field: Field; field: Field;
isInvalid: boolean; isInvalid: boolean;
backgroundColor?: string;
} }
function TagInput<Field extends Partial<ControllerRenderProps<FieldValues, 'tag'>>>({ field, isInvalid }: Props<Field>) { function TagInput<Field extends Partial<ControllerRenderProps<FieldValues, 'tag'>>>({ field, isInvalid, backgroundColor }: Props<Field>) {
return ( return (
<FormControl variant="floating" id="tag" isRequired> <FormControl variant="floating" id="tag" isRequired backgroundColor={ backgroundColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ isInvalid } isInvalid={ isInvalid }
......
...@@ -11,11 +11,12 @@ const HASH_LENGTH = 66; ...@@ -11,11 +11,12 @@ const HASH_LENGTH = 66;
type Props<Field> = { type Props<Field> = {
field: Field; field: Field;
isInvalid: boolean; isInvalid: boolean;
backgroundColor?: string;
} }
function AddressInput<Field extends Partial<ControllerRenderProps<FieldValues, 'transaction'>>>({ field, isInvalid }: Props<Field>) { function AddressInput<Field extends Partial<ControllerRenderProps<FieldValues, 'transaction'>>>({ field, isInvalid, backgroundColor }: Props<Field>) {
return ( return (
<FormControl variant="floating" id="transaction" isRequired> <FormControl variant="floating" id="transaction" isRequired backgroundColor={ backgroundColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ isInvalid } isInvalid={ isInvalid }
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
Text, Text,
Grid, Grid,
GridItem, GridItem,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
...@@ -59,6 +60,7 @@ type Checkboxes = 'notification' | ...@@ -59,6 +60,7 @@ type Checkboxes = 'notification' |
const AddressForm: React.FC<Props> = ({ data, onClose }) => { const AddressForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -106,12 +108,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose }) => { ...@@ -106,12 +108,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose }) => {
}, [ setValue, data ]); }, [ setValue, data ]);
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) }/>; return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>; return <TagInput field={ field } isInvalid={ Boolean(errors.tag) } backgroundColor={ formBackgroundColor }/>;
}, [ errors ]); }, [ errors, formBackgroundColor ]);
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => ( const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
......
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