Commit d89a6e2b authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #61 from blockscout/react-query

react-query. start.
parents 6d8c3f21 5f5e99cc
export const privateTagsAddress = [
{
address: '0x4831c121879d3de0e2b181d9d55e9b0724f5d926',
tag: 'some_tag',
},
{
address: '0x8c461F78760988c4135e363a87dd736f8b671ff0',
tag: 'some_other_tag',
},
{
address: '0x930F381E649c84579Ef58117E923714964C55D16',
tag: '12345678901234567890123456789012345',
},
];
export type TPrivateTagsAddress = Array<TPrivateTagsAddressItem>
export type TPrivateTagsAddressItem = {
address: string;
tag: string;
}
import React from 'react';
import React, { useState } from 'react';
import type { AppProps } from 'next/app';
import { ChakraProvider } from '@chakra-ui/react';
import theme from 'theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function MyApp({ Component, pageProps }: AppProps) {
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
}));
return (
<ChakraProvider theme={ theme }>
<Component { ...pageProps }/>
</ChakraProvider>
<QueryClientProvider client={ queryClient }>
<ChakraProvider theme={ theme }>
<Component { ...pageProps }/>
</ChakraProvider>
</QueryClientProvider>
);
}
......
import type { NextApiRequest, NextApiResponse } from 'next'
import type { NextApiRequest } from 'next'
import fetch from 'pages/api/utils/fetch';
import handler from 'pages/api/utils/handler';
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
const { id } = _req.query;
const url = `/account/v1/user/tags/address/${ id }`;
const getUrl = (req: NextApiRequest) => `/account/v1/user/tags/address/${ req.query.id }`
switch (_req.method) {
case 'DELETE': {
await fetch(url, { method: 'DELETE' })
res.status(200);
break;
}
const addressDeleteHandler = handler(getUrl, [ 'DELETE' ]);
default: {
res.setHeader('Allow', [ 'DELETE' ])
res.status(405).end(`Method ${ _req.method } Not Allowed`)
}
}
}
export default addressDeleteHandler;
import type { NextApiRequest, NextApiResponse } from 'next'
import handler from 'pages/api/utils/handler';
import fetch from 'pages/api/utils/fetch';
import type { AddressTags } from 'pages/api/types/account';
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
const url = '/account/v1/user/tags/address';
const addressHandler = handler<AddressTags>(() => '/account/v1/user/tags/address', [ 'GET', 'POST' ]);
switch (_req.method) {
case 'GET': {
const response = await fetch(url)
const data = await response.json();
res.status(200).json(data)
break;
}
case 'POST': {
const response = await fetch(url, {
method: 'POST',
body: _req.body,
})
const data = await response.json();
res.status(200).json(data)
break;
}
default: {
res.setHeader('Allow', [ 'GET', 'POST' ])
res.status(405).end(`Method ${ _req.method } Not Allowed`)
}
}
}
export default addressHandler;
import type { NextApiRequest } from 'next'
import handler from 'pages/api/utils/handler';
const getUrl = (req: NextApiRequest) => `/account/v1/user/tags/transaction/${ req.query.id }`
const transactionDeleteHandler = handler(getUrl, [ 'DELETE' ]);
export default transactionDeleteHandler;
import handler from 'pages/api/utils/handler';
import type { TransactionTags } from 'pages/api/types/account';
const transactionHandler = handler<TransactionTags>(() => '/account/v1/user/tags/transaction', [ 'GET', 'POST' ]);
export default transactionHandler;
// FIXME: here are types of the elixir api's responses
// and in types/api/ folder we have types of the node api's responses
// maybe they are always the same and there is no need to keep two separate files with types
export interface AddressTag {
address_hash: string;
name: string;
id: string;
}
export type AddressTags = Array<AddressTag>
export interface ApiKey {
apiKey: string;
apiKeyName: string;
}
export type ApiKeys = Array<ApiKey>
export interface ModelError {
message: string;
}
export interface NotificationDirection {
incoming: boolean;
outcoming: boolean;
}
export interface NotificationSettings {
_native?: NotificationDirection;
erc20?: NotificationDirection;
erc7211155?: NotificationDirection;
}
export interface Transaction {
fromAddressHash?: string;
toAddressHash?: string;
createdContractAddressHash?: string;
}
export interface TransactionTag {
transaction_hash: string;
name: string;
id: string;
}
export type TransactionTags = Array<TransactionTag>
export type Transactions = Array<Transaction>
export interface UserInfo {
name?: string;
nickname?: string;
email?: string;
}
export interface WatchlistAddress {
addressHash: string;
addressName: string;
addressBalance: number;
coinName: string;
exchangeRate?: number;
notificationSettings: NotificationSettings;
}
export interface WatchlistAddressNew {
addressName: string;
notificationSettings: NotificationSettings;
}
export type WatchlistAddresses = Array<WatchlistAddress>
import type { NextApiRequest, NextApiResponse } from 'next'
import fetch from './fetch';
type Methods = 'GET' | 'POST' | 'DELETE';
export default function handler<TRes>(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>) {
return async(_req: NextApiRequest, res: NextApiResponse<TRes>) => {
if (_req.method === 'GET' && allowedMethods.includes('GET')) {
const response = await fetch(getUrl(_req))
const data = await response.json() as TRes;
res.status(200).json(data);
} else if (allowedMethods.includes('POST') && _req.method === 'POST') {
const response = await fetch(getUrl(_req), {
method: 'POST',
body: _req.body,
})
const data = await response.json() as TRes;
res.status(200).json(data)
} else if (allowedMethods.includes('DELETE') && _req.method === 'DELETE') {
const response = await fetch(getUrl(_req), { method: 'DELETE' });
// FIXME: add error handlers
if (response.status !== 200) {
// eslint-disable-next-line no-console
console.log(response.statusText);
}
res.status(200).end();
} else {
res.setHeader('Allow', allowedMethods)
res.status(405).end(`Method ${ _req.method } Not Allowed`)
}
}
}
import React from 'react';
import React, { useCallback, useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head'
import { useQuery } from '@tanstack/react-query'
import PrivateTags from 'ui/pages/PrivateTags';
const TABS = [ 'address', 'transaction' ];
const PrivateTagsPage: NextPage = () => {
const [ activeTab, setActiveTab ] = useState(TABS[0]);
const onChangeTab = useCallback((index: number) => {
setActiveTab(TABS[index]);
}, [ setActiveTab ]);
// eslint-disable-next-line no-console
console.log(activeTab);
// FIXME: request data only for active tab and only once
// don't refetch after tab change
useQuery([ 'address' ], async() => {
const response = await fetch('/api/account/private-tags/address')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
useQuery([ 'transaction' ], async() => {
const response = await fetch('/api/account/private-tags/transaction')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
return (
<>
<Head><title>Private tags</title></Head>
<PrivateTags/>
<PrivateTags onChangeTab={ onChangeTab }/>
</>
);
}
export default PrivateTagsPage
export default PrivateTagsPage;
export interface AddressTag {
addressHash: string;
tagName: string;
visibilityLevel: boolean;
address_hash: string;
name: string;
id: string;
}
export type AddressTags = Array<AddressTag>
......@@ -35,9 +35,9 @@ export interface Transaction {
}
export interface TransactionTag {
transactionHash: string;
tagName: string;
visibilityLevel: boolean;
transaction_hash: string;
name: string;
id: string;
}
export type TransactionTags = Array<TransactionTag>
......
import React from 'react';
import React, { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query'
import type { AddressTags, TransactionTags } from 'types/api/account';
import {
Box,
......@@ -14,26 +17,34 @@ import AccountPageHeader from 'ui/shared/AccountPageHeader';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
const PrivateTags: React.FC = () => {
React.useEffect(() => {
fetch('/api/account/private-tags/address')
}, []);
type Props = {
onChangeTab: (index: number) => void;
}
const PrivateTags = ({ onChangeTab: onChangeTabProps }: Props) => {
const queryClient = useQueryClient();
const addressData = queryClient.getQueryData([ 'address' ]) as AddressTags;
const txData = queryClient.getQueryData([ 'transaction' ]) as TransactionTags;
const onTabChange = useCallback((index: number) => {
onChangeTabProps(index);
}, [ onChangeTabProps ])
return (
<Page>
<Box h="100%">
<AccountPageHeader text="Private tags"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onTabChange }>
<TabList marginBottom={ 8 }>
<Tab>Address</Tab>
<Tab>Transaction</Tab>
</TabList>
<TabPanels>
<TabPanel padding={ 0 }>
<PrivateAddressTags/>
<PrivateAddressTags addressTags={ addressData }/>
</TabPanel>
<TabPanel padding={ 0 }>
<PrivateTransactionTags/>
<PrivateTransactionTags transactionTags={ txData }/>
</TabPanel>
</TabPanels>
</Tabs>
......
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
Box,
Button,
......@@ -9,14 +11,14 @@ import {
import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import type { AddressTag } from 'types/api/account';
const ADDRESS_LENGTH = 42;
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TPrivateTagsAddressItem;
data?: AddressTag;
onClose: () => void;
}
type Inputs = {
......@@ -24,16 +26,40 @@ type Inputs = {
tag: string;
}
const AddressForm: React.FC<Props> = ({ data }) => {
const AddressForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => {
setValue('address', data?.address || '');
setValue('tag', data?.tag || '');
setValue('address', data?.address_hash || '');
setValue('tag', data?.name || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => {
return fetch('/api/account/private-tags/address', { method: 'POST', body: JSON.stringify({
name: formData?.tag,
address_hash: formData?.address,
}) })
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
queryClient.refetchQueries([ 'address' ]).then(() => {
onClose();
setPending(false);
});
},
});
const onSubmit: SubmitHandler<Inputs> = (formData) => {
setPending(true);
// api method for editing is not implemented now!!!
mutate(formData);
};
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) }/>
......@@ -72,6 +98,7 @@ const AddressForm: React.FC<Props> = ({ data }) => {
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
......
import React, { useCallback } from 'react';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import type { AddressTag } from 'types/api/account';
import AddressForm from './AddressForm';
import FormModal from 'ui/shared/FormModal';
......@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TPrivateTagsAddressItem;
data?: AddressTag;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
......@@ -16,10 +16,10 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const text = 'Label any address with a private address tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => {
return <AddressForm data={ data }/>
}, [ data ]);
return <AddressForm data={ data } onClose={ onClose }/>
}, [ data, onClose ]);
return (
<FormModal<TPrivateTagsAddressItem>
<FormModal<AddressTag>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
......
......@@ -9,14 +9,14 @@ import {
TableContainer,
} from '@chakra-ui/react'
import type { TPrivateTagsAddress, TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
data: TPrivateTagsAddress;
onEditClick: (data: TPrivateTagsAddressItem) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void;
data: AddressTags;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
......@@ -31,10 +31,10 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
{ data.map((item: AddressTag) => (
<AddressTagTableItem
item={ item }
key={ item.address }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......
......@@ -10,15 +10,15 @@ import {
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import type { AddressTag } from 'types/api/account';
import EditButton from 'ui/shared/EditButton';
import DeleteButton from 'ui/shared/DeleteButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props {
item: TPrivateTagsAddressItem;
onEditClick: (data: TPrivateTagsAddressItem) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void;
item: AddressTag;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
}
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
......@@ -31,17 +31,17 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.address }>
<Tr alignItems="top" key={ item.id }>
<Td>
<HStack spacing={ 4 }>
<AddressIcon address={ item.address }/>
<AddressLinkWithTooltip address={ item.address }/>
<AddressIcon address={ item.address_hash }/>
<AddressLinkWithTooltip address={ item.address_hash }/>
</HStack>
</Td>
<Td>
<TruncatedTextTooltip label={ item.tag }>
<TruncatedTextTooltip label={ item.name }>
<Tag variant="gray" lineHeight="24px">
{ item.tag }
{ item.name }
</Tag>
</TruncatedTextTooltip>
</Td>
......
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { Text } from '@chakra-ui/react';
import DeleteModal from 'ui/shared/DeleteModal'
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { AddressTag, TransactionTag } from 'types/api/account';
type Props = {
isOpen: boolean;
onClose: () => void;
tag?: string;
data?: AddressTag | TransactionTag;
type: 'address' | 'transaction';
}
const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => {
const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) => {
const [ pending, setPending ] = useState(false);
const tag = data?.name;
const id = data?.id;
const queryClient = useQueryClient();
const { mutate } = useMutation(() => {
return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' })
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
queryClient.refetchQueries([ type ]).then(() => {
onClose();
setPending(false);
});
},
});
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', tag);
}, [ tag ]);
setPending(true);
mutate()
}, [ mutate ]);
const renderText = useCallback(() => {
return (
<Text display="flex">Tag<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag || 'address' }" ` }</Text>will be deleted</Text>
<Text display="flex">Tag<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
)
}, [ tag ]);
......@@ -27,6 +54,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => {
onDelete={ onDelete }
title="Removal of private tag"
renderContent={ renderText }
pending={ pending }
/>
)
}
......
import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import { Box, Button, Spinner, Text, useDisclosure } from '@chakra-ui/react';
import AddressTagTable from './AddressTagTable/AddressTagTable';
import AddressModal from './AddressModal/AddressModal';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import { privateTagsAddress } from 'data/privateTagsAddress';
import DeletePrivateTagModal from './DeletePrivateTagModal';
import type { AddressTags, AddressTag } from 'types/api/account';
type Props = {
addressTags: AddressTags;
}
const PrivateAddressTags: React.FC = () => {
const PrivateAddressTags = ({ addressTags }: Props) => {
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ addressModalData, setAddressModalData ] = useState<TPrivateTagsAddressItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
const onEditClick = useCallback((data: TPrivateTagsAddressItem) => {
const onEditClick = useCallback((data: AddressTag) => {
setAddressModalData(data);
addressModalProps.onOpen();
}, [ addressModalProps ])
......@@ -26,8 +28,8 @@ const PrivateAddressTags: React.FC = () => {
addressModalProps.onClose();
}, [ addressModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsAddressItem) => {
setDeleteModalData(data.tag);
const onDeleteClick = useCallback((data: AddressTag) => {
setDeleteModalData(data);
deleteModalProps.onOpen();
}, [ deleteModalProps ])
......@@ -42,9 +44,10 @@ const PrivateAddressTags: React.FC = () => {
Use private transaction tags to label any transactions of interest.
Private tags are saved in your account and are only visible when you are logged in.
</Text>
{ Boolean(privateTagsAddress.length) && (
{ !addressTags && <Spinner/> }
{ Boolean(addressTags?.length) && (
<AddressTagTable
data={ privateTagsAddress }
data={ addressTags }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......@@ -59,7 +62,12 @@ const PrivateAddressTags: React.FC = () => {
</Button>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
<DeletePrivateTagModal { ...deleteModalProps } onClose={ onDeleteModalClose } tag={ deleteModalData }/>
<DeletePrivateTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
data={ deleteModalData }
type="address"
/>
</>
);
};
......
......@@ -2,21 +2,25 @@ import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
import TransactionModal from './TransactionModal/TransactionModal';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import { privateTagsTransaction } from 'data/privateTagsTransaction';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateTransactionTags: React.FC = () => {
type Props = {
transactionTags: TransactionTags;
}
const PrivateTransactionTags = ({ transactionTags }: Props) => {
const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ transactionModalData, setTransactionModalData ] = useState<TPrivateTagsTransactionItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
const onEditClick = useCallback((data: TPrivateTagsTransactionItem) => {
const onEditClick = useCallback((data: TransactionTag) => {
setTransactionModalData(data);
transactionModalProps.onOpen();
}, [ transactionModalProps ])
......@@ -26,8 +30,8 @@ const PrivateTransactionTags: React.FC = () => {
transactionModalProps.onClose();
}, [ transactionModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsTransactionItem) => {
setDeleteModalData(data.tag);
const onDeleteClick = useCallback((data: TransactionTag) => {
setDeleteModalData(data);
deleteModalProps.onOpen();
}, [ deleteModalProps ])
......@@ -42,9 +46,9 @@ const PrivateTransactionTags: React.FC = () => {
Use private transaction tags to label any transactions of interest.
Private tags are saved in your account and are only visible when you are logged in.
</Text>
{ Boolean(privateTagsTransaction.length) && (
{ Boolean(transactionTags.length) && (
<TransactionTagTable
data={ privateTagsTransaction }
data={ transactionTags }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......@@ -59,7 +63,12 @@ const PrivateTransactionTags: React.FC = () => {
</Button>
</Box>
<TransactionModal { ...transactionModalProps } onClose={ onAddressModalClose } data={ transactionModalData }/>
<DeletePrivateTagModal { ...deleteModalProps } onClose={ onDeleteModalClose } tag={ deleteModalData }/>
<DeletePrivateTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
data={ deleteModalData }
type="transaction"
/>
</>
);
};
......
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag } from 'types/api/account';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
Box,
Button,
......@@ -10,13 +14,12 @@ import {
import TransactionInput from 'ui/shared/TransactionInput';
import TagInput from 'ui/shared/TagInput';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
const HASH_LENGTH = 66;
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TPrivateTagsTransactionItem;
data?: TransactionTag;
onClose: () => void;
}
type Inputs = {
......@@ -24,16 +27,40 @@ type Inputs = {
tag: string;
}
const TransactionForm: React.FC<Props> = ({ data }) => {
const TransactionForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => {
setValue('transaction', data?.transaction || '');
setValue('tag', data?.tag || '');
setValue('transaction', data?.transaction_hash || '');
setValue('tag', data?.name || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => {
return fetch('/api/account/private-tags/transaction', { method: 'POST', body: JSON.stringify({
name: formData?.tag,
transaction_hash: formData?.transaction,
}) })
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
queryClient.refetchQueries([ 'transaction' ]).then(() => {
onClose();
setPending(false);
});
},
});
const onSubmit: SubmitHandler<Inputs> = formData => {
setPending(true);
// api method for editing is not implemented now!!!
mutate(formData)
}
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) }/>
......@@ -72,6 +99,7 @@ const TransactionForm: React.FC<Props> = ({ data }) => {
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
......
import React, { useCallback } from 'react';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import type { TransactionTag } from 'types/api/account';
import TransactionForm from './TransactionForm';
import FormModal from 'ui/shared/FormModal';
......@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TPrivateTagsTransactionItem;
data?: TransactionTag;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
......@@ -16,10 +16,10 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const text = 'Label any transaction with a private transaction tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => {
return <TransactionForm data={ data }/>
}, [ data ]);
return <TransactionForm data={ data } onClose={ onClose }/>
}, [ data, onClose ]);
return (
<FormModal<TPrivateTagsTransactionItem>
<FormModal<TransactionTag>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
......
import React from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import {
Table,
Thead,
......@@ -9,14 +11,12 @@ import {
TableContainer,
} from '@chakra-ui/react'
import type { TPrivateTagsTransaction, TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import TransactionTagTableItem from './TransactionTagTableItem';
interface Props {
data: TPrivateTagsTransaction;
onEditClick: (data: TPrivateTagsTransactionItem) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void;
data: TransactionTags;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
......@@ -34,7 +34,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
{ data.map((item) => (
<TransactionTagTableItem
item={ item }
key={ item.transaction }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......
......@@ -13,12 +13,12 @@ import DeleteButton from 'ui/shared/DeleteButton';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import type { TransactionTag } from 'types/api/account';
interface Props {
item: TPrivateTagsTransactionItem;
onEditClick: (data: TPrivateTagsTransactionItem) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void;
item: TransactionTag;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
}
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
......@@ -31,14 +31,14 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.transaction }>
<Tr alignItems="top" key={ item.id }>
<Td>
<AddressLinkWithTooltip address={ item.transaction }/>
<AddressLinkWithTooltip address={ item.transaction_hash }/>
</Td>
<Td>
<Tooltip label={ item.tag }>
<Tooltip label={ item.name }>
<Tag variant="gray" lineHeight="24px">
{ item.tag }
{ item.name }
</Tag>
</Tooltip>
</Td>
......
......@@ -17,14 +17,14 @@ type Props = {
onDelete: () => void;
title: string;
renderContent: () => JSX.Element;
pending?: boolean;
}
const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, renderContent }) => {
const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, renderContent, pending }) => {
const onDeleteClick = useCallback(() => {
onDelete();
onClose()
}, [ onClose, onDelete ]);
}, [ onDelete ]);
return (
<Modal isOpen={ isOpen } onClose={ onClose } size="md">
......@@ -36,7 +36,14 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, render
{ renderContent() }
</ModalBody>
<ModalFooter>
<Button variant="primary" size="lg" onClick={ onDeleteClick }>
<Button
variant="primary"
size="lg"
onClick={ onDeleteClick }
isLoading={ pending }
// FIXME: chackra's button is disabled when isLoading
disabled={ false }
>
Delete
</Button>
</ModalFooter>
......
......@@ -16,7 +16,7 @@ interface Props<TData> {
data?: TData;
title: string;
text: string;
renderForm: (data?: TData) => JSX.Element;
renderForm: () => JSX.Element;
}
export default function FormModal<TData>({ isOpen, onClose, data, title, text, renderForm }: Props<TData>) {
......@@ -32,7 +32,7 @@ export default function FormModal<TData>({ isOpen, onClose, data, title, text, r
{ text }
</Text>
) }
{ renderForm(data) }
{ renderForm() }
</ModalBody>
</ModalContent>
</Modal>
......
......@@ -875,6 +875,36 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0"
integrity sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==
"@tanstack/match-sorter-utils@^8.0.0-alpha.82":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834"
integrity sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==
dependencies:
remove-accents "0.4.2"
"@tanstack/query-core@^4.0.0-beta.1":
version "4.0.10"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.0.10.tgz#cae6f818006616dc72c95c863592f5f68b47548a"
integrity sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==
"@tanstack/react-query-devtools@^4.0.10":
version "4.0.10"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.0.10.tgz#d1b3e5b1917f1c22bcee5830ef7af1ccfc4879f4"
integrity sha512-3J7LLYQjfjTI0DbPo0bA+M3l4kdvYSWAqihpeG1u93WVyZj0OEFviUv+4cK7+k2AVgQJAPMZ5xvtewKxOOFVrw==
dependencies:
"@tanstack/match-sorter-utils" "^8.0.0-alpha.82"
"@types/use-sync-external-store" "^0.0.3"
use-sync-external-store "^1.2.0"
"@tanstack/react-query@^4.0.10":
version "4.0.10"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.0.10.tgz#92c71a2632c06450d848d4964959bd216cde03c0"
integrity sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==
dependencies:
"@tanstack/query-core" "^4.0.0-beta.1"
"@types/use-sync-external-store" "^0.0.3"
use-sync-external-store "^1.2.0"
"@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
......@@ -949,6 +979,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
"@typescript-eslint/eslint-plugin@^5.27.0":
version "5.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8"
......@@ -2939,6 +2974,11 @@ regexpp@^3.2.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
......@@ -3405,6 +3445,11 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
......
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