Commit 6e893339 authored by isstuev's avatar isstuev

react-query. start.

parent 6d8c3f21
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 type { AppProps } from 'next/app';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import theme from 'theme'; import theme from 'theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
}));
return ( return (
<ChakraProvider theme={ theme }> <QueryClientProvider client={ queryClient }>
<Component { ...pageProps }/> <ChakraProvider theme={ theme }>
</ChakraProvider> <Component { ...pageProps }/>
</ChakraProvider>
</QueryClientProvider>
); );
} }
......
...@@ -8,8 +8,13 @@ export default async function handler(_req: NextApiRequest, res: NextApiResponse ...@@ -8,8 +8,13 @@ export default async function handler(_req: NextApiRequest, res: NextApiResponse
switch (_req.method) { switch (_req.method) {
case 'DELETE': { case 'DELETE': {
await fetch(url, { method: 'DELETE' }) const response = await fetch(url, { method: 'DELETE' });
res.status(200); // FIXME: add error handlers
if (response.status !== 200) {
// eslint-disable-next-line no-console
console.log(response.statusText);
}
res.status(200).end();
break; break;
} }
......
import type { NextApiRequest, NextApiResponse } from 'next'
import fetch from 'api/utils/fetch';
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
const { id } = _req.query;
const url = `/account/v1/user/tags/transaction/${ id }`;
switch (_req.method) {
case 'DELETE': {
const response = await fetch(url, { method: 'DELETE' });
// FIXME: add error handlers
if (response.status !== 200) {
// eslint-disable-next-line no-console
console.log(response.statusText);
}
res.status(200).end();
break;
}
default: {
res.setHeader('Allow', [ 'DELETE' ])
res.status(405).end(`Method ${ _req.method } Not Allowed`)
}
}
}
import type { NextApiRequest, NextApiResponse } from 'next'
import fetch from 'api/utils/fetch';
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
const url = '/account/v1/user/tags/transaction';
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`)
}
}
}
import React from 'react'; import React, { useCallback, useState } from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head' import Head from 'next/head'
import { useQuery } from '@tanstack/react-query'
import PrivateTags from 'ui/pages/PrivateTags'; import PrivateTags from 'ui/pages/PrivateTags';
const TABS = [ 'address', 'transaction' ];
const PrivateTagsPage: NextPage = () => { 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 ( return (
<> <>
<Head><title>Private tags</title></Head> <Head><title>Private tags</title></Head>
<PrivateTags/> <PrivateTags onChangeTab={ onChangeTab }/>
</> </>
); );
} }
export default PrivateTagsPage export default PrivateTagsPage;
export interface AddressTag { export interface AddressTag {
addressHash: string; address_hash: string;
tagName: string; name: string;
visibilityLevel: boolean; id: string;
} }
export type AddressTags = Array<AddressTag> export type AddressTags = Array<AddressTag>
...@@ -35,9 +35,9 @@ export interface Transaction { ...@@ -35,9 +35,9 @@ export interface Transaction {
} }
export interface TransactionTag { export interface TransactionTag {
transactionHash: string; transaction_hash: string;
tagName: string; name: string;
visibilityLevel: boolean; id: string;
} }
export type TransactionTags = Array<TransactionTag> 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 { import {
Box, Box,
...@@ -14,26 +17,34 @@ import AccountPageHeader from 'ui/shared/AccountPageHeader'; ...@@ -14,26 +17,34 @@ import AccountPageHeader from 'ui/shared/AccountPageHeader';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
const PrivateTags: React.FC = () => { type Props = {
React.useEffect(() => { onChangeTab: (index: number) => void;
fetch('/api/account/private-tags/address') }
}, []);
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 ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<AccountPageHeader text="Private tags"/> <AccountPageHeader text="Private tags"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy> <Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onTabChange }>
<TabList marginBottom={ 8 }> <TabList marginBottom={ 8 }>
<Tab>Address</Tab> <Tab>Address</Tab>
<Tab>Transaction</Tab> <Tab>Transaction</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel padding={ 0 }> <TabPanel padding={ 0 }>
<PrivateAddressTags/> <PrivateAddressTags addressTags={ addressData }/>
</TabPanel> </TabPanel>
<TabPanel padding={ 0 }> <TabPanel padding={ 0 }>
<PrivateTransactionTags/> <PrivateTransactionTags transactionTags={ txData }/>
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>
......
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { import {
Box, Box,
Button, Button,
...@@ -9,14 +11,14 @@ import { ...@@ -9,14 +11,14 @@ import {
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
import type { AddressTag } from 'types/api/account';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
const ADDRESS_LENGTH = 42; const ADDRESS_LENGTH = 42;
const TAG_MAX_LENGTH = 35; const TAG_MAX_LENGTH = 35;
type Props = { type Props = {
data?: TPrivateTagsAddressItem; data?: AddressTag;
onClose: () => void;
} }
type Inputs = { type Inputs = {
...@@ -24,16 +26,40 @@ type Inputs = { ...@@ -24,16 +26,40 @@ type Inputs = {
tag: string; 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>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => { useEffect(() => {
setValue('address', data?.address || ''); setValue('address', data?.address_hash || '');
setValue('tag', data?.tag || ''); setValue('tag', data?.name || '');
}, [ setValue, data ]); }, [ setValue, data ]);
// eslint-disable-next-line no-console const queryClient = useQueryClient();
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
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'>}) => { 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) }/>
...@@ -72,6 +98,7 @@ const AddressForm: React.FC<Props> = ({ data }) => { ...@@ -72,6 +98,7 @@ const AddressForm: React.FC<Props> = ({ data }) => {
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ Object.keys(errors).length > 0 }
isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
</Button> </Button>
......
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress'; import type { AddressTag } from 'types/api/account';
import AddressForm from './AddressForm'; import AddressForm from './AddressForm';
import FormModal from 'ui/shared/FormModal'; import FormModal from 'ui/shared/FormModal';
...@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal'; ...@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
data?: TPrivateTagsAddressItem; data?: AddressTag;
} }
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
...@@ -16,10 +16,10 @@ 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 text = 'Label any address with a private address tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => { const renderForm = useCallback(() => {
return <AddressForm data={ data }/> return <AddressForm data={ data } onClose={ onClose }/>
}, [ data ]); }, [ data, onClose ]);
return ( return (
<FormModal<TPrivateTagsAddressItem> <FormModal<AddressTag>
isOpen={ isOpen } isOpen={ isOpen }
onClose={ onClose } onClose={ onClose }
title={ title } title={ title }
......
...@@ -9,14 +9,14 @@ import { ...@@ -9,14 +9,14 @@ import {
TableContainer, TableContainer,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import type { TPrivateTagsAddress, TPrivateTagsAddressItem } from 'data/privateTagsAddress'; import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem'; import AddressTagTableItem from './AddressTagTableItem';
interface Props { interface Props {
data: TPrivateTagsAddress; data: AddressTags;
onEditClick: (data: TPrivateTagsAddressItem) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void; onDeleteClick: (data: AddressTag) => void;
} }
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
...@@ -31,10 +31,10 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -31,10 +31,10 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item: AddressTag) => (
<AddressTagTableItem <AddressTagTableItem
item={ item } item={ item }
key={ item.address } key={ item.id }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
...@@ -10,15 +10,15 @@ import { ...@@ -10,15 +10,15 @@ import {
import AddressIcon from 'ui/shared/AddressIcon'; import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; 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 EditButton from 'ui/shared/EditButton';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: TPrivateTagsAddressItem; item: AddressTag;
onEditClick: (data: TPrivateTagsAddressItem) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void; onDeleteClick: (data: AddressTag) => void;
} }
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
...@@ -31,17 +31,17 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,17 +31,17 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<Tr alignItems="top" key={ item.address }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<HStack spacing={ 4 }> <HStack spacing={ 4 }>
<AddressIcon address={ item.address }/> <AddressIcon address={ item.address_hash }/>
<AddressLinkWithTooltip address={ item.address }/> <AddressLinkWithTooltip address={ item.address_hash }/>
</HStack> </HStack>
</Td> </Td>
<Td> <Td>
<TruncatedTextTooltip label={ item.tag }> <TruncatedTextTooltip label={ item.name }>
<Tag variant="gray" lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ item.tag } { item.name }
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Td> </Td>
......
import React, { useCallback } from 'react'; import React, { useCallback, useState } from 'react';
import { Text } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import DeleteModal from 'ui/shared/DeleteModal' import DeleteModal from 'ui/shared/DeleteModal'
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { AddressTag, TransactionTag } from 'types/api/account';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; 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(() => { const onDelete = useCallback(() => {
// eslint-disable-next-line no-console setPending(true);
console.log('delete', tag); mutate()
}, [ tag ]); }, [ mutate ]);
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( 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 ]); }, [ tag ]);
...@@ -27,6 +54,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => { ...@@ -27,6 +54,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => {
onDelete={ onDelete } onDelete={ onDelete }
title="Removal of private tag" title="Removal of private tag"
renderContent={ renderText } renderContent={ renderText }
pending={ pending }
/> />
) )
} }
......
import React, { useCallback, useState } from 'react'; 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 AddressTagTable from './AddressTagTable/AddressTagTable';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import { privateTagsAddress } from 'data/privateTagsAddress';
import DeletePrivateTagModal from './DeletePrivateTagModal'; 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 addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ addressModalData, setAddressModalData ] = useState<TPrivateTagsAddressItem>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<string>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
const onEditClick = useCallback((data: TPrivateTagsAddressItem) => { const onEditClick = useCallback((data: AddressTag) => {
setAddressModalData(data); setAddressModalData(data);
addressModalProps.onOpen(); addressModalProps.onOpen();
}, [ addressModalProps ]) }, [ addressModalProps ])
...@@ -26,8 +28,8 @@ const PrivateAddressTags: React.FC = () => { ...@@ -26,8 +28,8 @@ const PrivateAddressTags: React.FC = () => {
addressModalProps.onClose(); addressModalProps.onClose();
}, [ addressModalProps ]); }, [ addressModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsAddressItem) => { const onDeleteClick = useCallback((data: AddressTag) => {
setDeleteModalData(data.tag); setDeleteModalData(data);
deleteModalProps.onOpen(); deleteModalProps.onOpen();
}, [ deleteModalProps ]) }, [ deleteModalProps ])
...@@ -42,9 +44,10 @@ const PrivateAddressTags: React.FC = () => { ...@@ -42,9 +44,10 @@ const PrivateAddressTags: React.FC = () => {
Use private transaction tags to label any transactions of interest. 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. Private tags are saved in your account and are only visible when you are logged in.
</Text> </Text>
{ Boolean(privateTagsAddress.length) && ( { !addressTags && <Spinner/> }
{ Boolean(addressTags?.length) && (
<AddressTagTable <AddressTagTable
data={ privateTagsAddress } data={ addressTags }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
...@@ -59,7 +62,12 @@ const PrivateAddressTags: React.FC = () => { ...@@ -59,7 +62,12 @@ const PrivateAddressTags: React.FC = () => {
</Button> </Button>
</Box> </Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/> <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'; ...@@ -2,21 +2,25 @@ import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
import TransactionModal from './TransactionModal/TransactionModal'; import TransactionModal from './TransactionModal/TransactionModal';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import { privateTagsTransaction } from 'data/privateTagsTransaction';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateTransactionTags: React.FC = () => { type Props = {
transactionTags: TransactionTags;
}
const PrivateTransactionTags = ({ transactionTags }: Props) => {
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ transactionModalData, setTransactionModalData ] = useState<TPrivateTagsTransactionItem>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<string>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
const onEditClick = useCallback((data: TPrivateTagsTransactionItem) => { const onEditClick = useCallback((data: TransactionTag) => {
setTransactionModalData(data); setTransactionModalData(data);
transactionModalProps.onOpen(); transactionModalProps.onOpen();
}, [ transactionModalProps ]) }, [ transactionModalProps ])
...@@ -26,8 +30,8 @@ const PrivateTransactionTags: React.FC = () => { ...@@ -26,8 +30,8 @@ const PrivateTransactionTags: React.FC = () => {
transactionModalProps.onClose(); transactionModalProps.onClose();
}, [ transactionModalProps ]); }, [ transactionModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsTransactionItem) => { const onDeleteClick = useCallback((data: TransactionTag) => {
setDeleteModalData(data.tag); setDeleteModalData(data);
deleteModalProps.onOpen(); deleteModalProps.onOpen();
}, [ deleteModalProps ]) }, [ deleteModalProps ])
...@@ -42,9 +46,9 @@ const PrivateTransactionTags: React.FC = () => { ...@@ -42,9 +46,9 @@ const PrivateTransactionTags: React.FC = () => {
Use private transaction tags to label any transactions of interest. 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. Private tags are saved in your account and are only visible when you are logged in.
</Text> </Text>
{ Boolean(privateTagsTransaction.length) && ( { Boolean(transactionTags.length) && (
<TransactionTagTable <TransactionTagTable
data={ privateTagsTransaction } data={ transactionTags }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
...@@ -59,7 +63,12 @@ const PrivateTransactionTags: React.FC = () => { ...@@ -59,7 +63,12 @@ const PrivateTransactionTags: React.FC = () => {
</Button> </Button>
</Box> </Box>
<TransactionModal { ...transactionModalProps } onClose={ onAddressModalClose } data={ transactionModalData }/> <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 type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } 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 { import {
Box, Box,
Button, Button,
...@@ -10,13 +14,12 @@ import { ...@@ -10,13 +14,12 @@ import {
import TransactionInput from 'ui/shared/TransactionInput'; import TransactionInput from 'ui/shared/TransactionInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
const HASH_LENGTH = 66; const HASH_LENGTH = 66;
const TAG_MAX_LENGTH = 35; const TAG_MAX_LENGTH = 35;
type Props = { type Props = {
data?: TPrivateTagsTransactionItem; data?: TransactionTag;
onClose: () => void;
} }
type Inputs = { type Inputs = {
...@@ -24,16 +27,40 @@ type Inputs = { ...@@ -24,16 +27,40 @@ type Inputs = {
tag: string; 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>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => { useEffect(() => {
setValue('transaction', data?.transaction || ''); setValue('transaction', data?.transaction_hash || '');
setValue('tag', data?.tag || ''); setValue('tag', data?.name || '');
}, [ setValue, data ]); }, [ setValue, data ]);
// eslint-disable-next-line no-console const queryClient = useQueryClient();
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
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'>}) => { const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) }/> return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) }/>
...@@ -72,6 +99,7 @@ const TransactionForm: React.FC<Props> = ({ data }) => { ...@@ -72,6 +99,7 @@ const TransactionForm: React.FC<Props> = ({ data }) => {
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ Object.keys(errors).length > 0 }
isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
</Button> </Button>
......
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction'; import type { TransactionTag } from 'types/api/account';
import TransactionForm from './TransactionForm'; import TransactionForm from './TransactionForm';
import FormModal from 'ui/shared/FormModal'; import FormModal from 'ui/shared/FormModal';
...@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal'; ...@@ -8,7 +8,7 @@ import FormModal from 'ui/shared/FormModal';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
data?: TPrivateTagsTransactionItem; data?: TransactionTag;
} }
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
...@@ -16,10 +16,10 @@ 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 text = 'Label any transaction with a private transaction tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => { const renderForm = useCallback(() => {
return <TransactionForm data={ data }/> return <TransactionForm data={ data } onClose={ onClose }/>
}, [ data ]); }, [ data, onClose ]);
return ( return (
<FormModal<TPrivateTagsTransactionItem> <FormModal<TransactionTag>
isOpen={ isOpen } isOpen={ isOpen }
onClose={ onClose } onClose={ onClose }
title={ title } title={ title }
......
import React from 'react'; import React from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import { import {
Table, Table,
Thead, Thead,
...@@ -9,14 +11,12 @@ import { ...@@ -9,14 +11,12 @@ import {
TableContainer, TableContainer,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import type { TPrivateTagsTransaction, TPrivateTagsTransactionItem } from 'data/privateTagsTransaction';
import TransactionTagTableItem from './TransactionTagTableItem'; import TransactionTagTableItem from './TransactionTagTableItem';
interface Props { interface Props {
data: TPrivateTagsTransaction; data: TransactionTags;
onEditClick: (data: TPrivateTagsTransactionItem) => void; onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
...@@ -34,7 +34,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -34,7 +34,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
{ data.map((item) => ( { data.map((item) => (
<TransactionTagTableItem <TransactionTagTableItem
item={ item } item={ item }
key={ item.transaction } key={ item.id }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
...@@ -13,12 +13,12 @@ import DeleteButton from 'ui/shared/DeleteButton'; ...@@ -13,12 +13,12 @@ import DeleteButton from 'ui/shared/DeleteButton';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import type { TPrivateTagsTransactionItem } from 'data/privateTagsTransaction'; import type { TransactionTag } from 'types/api/account';
interface Props { interface Props {
item: TPrivateTagsTransactionItem; item: TransactionTag;
onEditClick: (data: TPrivateTagsTransactionItem) => void; onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
...@@ -31,14 +31,14 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,14 +31,14 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<Tr alignItems="top" key={ item.transaction }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<AddressLinkWithTooltip address={ item.transaction }/> <AddressLinkWithTooltip address={ item.transaction_hash }/>
</Td> </Td>
<Td> <Td>
<Tooltip label={ item.tag }> <Tooltip label={ item.name }>
<Tag variant="gray" lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ item.tag } { item.name }
</Tag> </Tag>
</Tooltip> </Tooltip>
</Td> </Td>
......
...@@ -17,14 +17,14 @@ type Props = { ...@@ -17,14 +17,14 @@ type Props = {
onDelete: () => void; onDelete: () => void;
title: string; title: string;
renderContent: () => JSX.Element; 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(() => { const onDeleteClick = useCallback(() => {
onDelete(); onDelete();
onClose() }, [ onDelete ]);
}, [ onClose, onDelete ]);
return ( return (
<Modal isOpen={ isOpen } onClose={ onClose } size="md"> <Modal isOpen={ isOpen } onClose={ onClose } size="md">
...@@ -36,7 +36,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, render ...@@ -36,7 +36,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, render
{ renderContent() } { renderContent() }
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="primary" size="lg" onClick={ onDeleteClick }> <Button variant="primary" size="lg" onClick={ onDeleteClick } isLoading={ pending }>
Delete Delete
</Button> </Button>
</ModalFooter> </ModalFooter>
......
...@@ -16,7 +16,7 @@ interface Props<TData> { ...@@ -16,7 +16,7 @@ interface Props<TData> {
data?: TData; data?: TData;
title: string; title: string;
text: string; text: string;
renderForm: (data?: TData) => JSX.Element; renderForm: () => JSX.Element;
} }
export default function FormModal<TData>({ isOpen, onClose, data, title, text, renderForm }: Props<TData>) { 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 ...@@ -32,7 +32,7 @@ export default function FormModal<TData>({ isOpen, onClose, data, title, text, r
{ text } { text }
</Text> </Text>
) } ) }
{ renderForm(data) } { renderForm() }
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
......
...@@ -875,6 +875,36 @@ ...@@ -875,6 +875,36 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0"
integrity sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw== 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": "@types/json-schema@^7.0.9":
version "7.0.11" version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
...@@ -949,6 +979,11 @@ ...@@ -949,6 +979,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 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": "@typescript-eslint/eslint-plugin@^5.27.0":
version "5.27.0" version "5.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8"
...@@ -2939,6 +2974,11 @@ regexpp@^3.2.0: ...@@ -2939,6 +2974,11 @@ regexpp@^3.2.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== 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: resolve-from@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
...@@ -3405,6 +3445,11 @@ use-sidecar@^1.1.2: ...@@ -3405,6 +3445,11 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0" detect-node-es "^1.1.0"
tslib "^2.0.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: v8-compile-cache@^2.0.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" 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