Commit 94ed0ff7 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #412 from blockscout/address-favorite

address page: add to watch list
parents 6c197428 5b70e91e
import React from 'react';
// prevent set focus on button when closing modal
export default function usePreventFocusAfterModalClosing() {
return React.useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []);
}
......@@ -51,15 +51,15 @@ const variantOutline = defineStyle((props) => {
const activeBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props);
const activeColor = (() => {
if (c === 'gray') {
return mode('blue.400', 'gray.50')(props);
return mode('blue.600', 'gray.50')(props);
}
if (c === 'gray-dark') {
return mode('blue.700', 'gray.50')(props);
return mode('blue.600', 'gray.50')(props);
}
if (c === 'blue') {
return mode('blue.400', 'gray.50')(props);
return mode('blue.600', 'gray.50')(props);
}
return 'blue.400';
return 'blue.600';
})();
return {
......
import type { AddressParam } from './addressParams';
export interface AddressTag {
address_hash: string;
name: string;
......@@ -63,6 +64,7 @@ export interface WatchlistAddress {
notification_settings: NotificationSettings;
notification_methods: NotificationMethods;
id: string;
address?: AddressParam;
}
export interface WatchlistAddressNew {
......
import { Box, Flex, Text, Icon, Button, Grid } from '@chakra-ui/react';
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
......@@ -10,7 +10,6 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import metamaskIcon from 'icons/metamask.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -21,6 +20,7 @@ import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton';
import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect';
......@@ -69,9 +69,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
</Text>
<CopyToClipboard text={ addressQuery.data.hash }/>
<Icon as={ metamaskIcon } boxSize={ 6 } ml={ 2 }/>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ starOutlineIcon } boxSize={ 5 }/>
</Button>
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/>
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/>
</Flex>
{ explorers.length > 0 && (
......
import { Icon, chakra, Tooltip, IconButton, useDisclosure } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import type { TWatchlist } from 'types/client/account';
import { QueryKeys as AccountQueryKeys } from 'types/client/accountQueries';
import { QueryKeys } from 'types/client/queries';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useFetch from 'lib/hooks/useFetch';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
interface Props {
className?: string;
hash: string;
isAdded: boolean;
}
const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const addModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const fetch = useFetch();
const profileData = queryClient.getQueryData<UserInfo>([ AccountQueryKeys.profile ]);
const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl();
const watchListQuery = useQuery<unknown, unknown, TWatchlist>(
[ AccountQueryKeys.watchlist ],
async() => fetch('/node-api/account/watchlist'),
{
enabled: isAdded,
},
);
const handleClick = React.useCallback(() => {
if (!isAuth) {
window.location.assign(loginUrl);
return;
}
isAdded ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
await queryClient.refetchQueries({ queryKey: [ QueryKeys.address, router.query.id ] });
addModalProps.onClose();
}, [ addModalProps, queryClient, router.query.id ]);
const handleAddModalClose = React.useCallback(() => {
addModalProps.onClose();
}, [ addModalProps ]);
const handleDeleteModalClose = React.useCallback(() => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const formData = React.useMemo(() => {
return {
address_hash: hash,
// FIXME temporary solution
// there is no endpoint in api what can return watchlist address info by its hash
// so we look up in the whole watchlist and hope we can find a necessary item
id: watchListQuery.data?.find((address) => address.address?.hash === hash)?.id || '',
};
}, [ hash, watchListQuery.data ]);
return (
<>
<Tooltip label={ `${ isAdded ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<IconButton
isActive={ isAdded }
className={ className }
aria-label="edit"
variant="outline"
size="sm"
pl="6px"
pr="6px"
onClick={ handleClick }
icon={ <Icon as={ isAdded ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
onFocusCapture={ usePreventFocusAfterModalClosing() }
/>
</Tooltip>
<WatchlistAddModal
{ ...addModalProps }
isAdd
onClose={ handleAddModalClose }
onSuccess={ handleAddOrDeleteSuccess }
data={ formData }
/>
<DeleteAddressModal
{ ...deleteModalProps }
onClose={ handleDeleteModalClose }
data={ formData }
onSuccess={ handleAddOrDeleteSuccess }
/>
</>
);
};
export default chakra(AddressFavoriteButton);
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account';
......@@ -22,6 +22,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
const { data, isLoading, isError } =
useQuery<unknown, unknown, TWatchlist>([ QueryKeys.watchlist ], async() => fetch('/node-api/account/watchlist/get-with-tokens'));
const queryClient = useQueryClient();
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
......@@ -42,6 +43,12 @@ const WatchList: React.FC = () => {
addressModalProps.onClose();
}, [ addressModalProps ]);
const onAddOrEditSuccess = useCallback(async() => {
await queryClient.refetchQueries([ QueryKeys.watchlist ]);
setAddressModalData(undefined);
addressModalProps.onClose();
}, [ addressModalProps, queryClient ]);
const onDeleteClick = useCallback((data: TWatchlistItem) => {
setDeleteModalData(data);
deleteModalProps.onOpen();
......@@ -52,6 +59,12 @@ const WatchList: React.FC = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ QueryKeys.watchlist ], (prevData: TWatchlist | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
}, [ deleteModalData?.id, queryClient ]);
const description = (
<AccountPageDescription>
An email notification can be sent to you when an address on your watch list sends or receives any transactions.
......@@ -107,8 +120,21 @@ const WatchList: React.FC = () => {
Add address
</Button>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
{ deleteModalData && <DeleteAddressModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
<AddressModal
{ ...addressModalProps }
onClose={ onAddressModalClose }
onSuccess={ onAddOrEditSuccess }
data={ addressModalData }
isAdd={ !addressModalData }
/>
{ deleteModalData && (
<DeleteAddressModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
onSuccess={ onDeleteSuccess }
data={ deleteModalData }
/>
) }
</>
);
}
......
import { Tooltip, IconButton, Icon, HStack } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import React from 'react';
import DeleteIcon from 'icons/delete.svg';
import EditIcon from 'icons/edit.svg';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
type Props = {
onEditClick: () => void;
......@@ -10,8 +11,7 @@ type Props = {
}
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => {
// prevent set focus on button when closing modal
const onFocusCapture = useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []);
const onFocusCapture = usePreventFocusAfterModalClosing();
return (
<HStack spacing={ 6 } alignSelf="flex-end">
......
......@@ -4,14 +4,13 @@ import {
Text,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
......@@ -29,9 +28,10 @@ const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TWatchlistItem;
onClose: () => void;
data?: Partial<TWatchlistItem>;
onSuccess: () => Promise<void>;
setAlertVisible: (isAlertVisible: boolean) => void;
isAdd: boolean;
}
type Inputs = {
......@@ -62,12 +62,12 @@ type Checkboxes = 'notification' |
'notification_settings.ERC-721.outcoming' |
'notification_settings.ERC-721.incoming';
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => {
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
let notificationsDefault = {} as Inputs['notification_settings'];
if (!data) {
if (!data?.notification_settings) {
NOTIFICATIONS.forEach(n => notificationsDefault[n] = { incoming: true, outcoming: true });
} else {
notificationsDefault = data.notification_settings;
......@@ -77,13 +77,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
notification: data ? data.notification_methods.email : true,
notification: data?.notification_methods ? data.notification_methods.email : true,
notification_settings: notificationsDefault,
},
mode: 'onTouched',
});
const queryClient = useQueryClient();
const fetch = useFetch();
function updateWatchlist(formData: Inputs) {
......@@ -95,7 +94,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
email: formData.notification,
},
};
if (data) {
if (!isAdd && data) {
// edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/node-api/account/watchlist/${ data.id }`, { method: 'PUT', body });
......@@ -106,11 +105,9 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}
const { mutate } = useMutation(updateWatchlist, {
onSuccess: () => {
queryClient.refetchQueries([ QueryKeys.watchlist ]).then(() => {
onClose();
onSuccess: async() => {
await onSuccess();
setPending(false);
});
},
onError: (e: ErrorType<WatchlistErrors>) => {
setPending(false);
......@@ -193,7 +190,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isLoading={ pending }
disabled={ !isValid || !isDirty }
>
{ data ? 'Save changes' : 'Add address' }
{ !isAdd ? 'Save changes' : 'Add address' }
</Button>
</Box>
</form>
......
......@@ -7,20 +7,22 @@ import FormModal from 'ui/shared/FormModal';
import AddressForm from './AddressForm';
type Props = {
isAdd: boolean;
isOpen: boolean;
onClose: () => void;
data?: TWatchlistItem;
onSuccess: () => Promise<void>;
data?: Partial<TWatchlistItem>;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit watch list address' : 'New address to watch list';
const text = !data ? 'An email notification can be sent to you when an address on your watch list sends or receives any transactions.' : '';
const AddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data, isAdd }) => {
const title = !isAdd ? 'Edit watch list address' : 'New address to watch list';
const text = isAdd ? 'An email notification can be sent to you when an address on your watch list sends or receives any transactions.' : '';
const [ isAlertVisible, setAlertVisible ] = useState(false);
const renderForm = useCallback(() => {
return <AddressForm data={ data } onClose={ onClose } setAlertVisible={ setAlertVisible }/>;
}, [ data, onClose ]);
return <AddressForm data={ data } onSuccess={ onSuccess } setAlertVisible={ setAlertVisible } isAdd={ isAdd }/>;
}, [ data, isAdd, onSuccess ]);
return (
<FormModal<TWatchlistItem>
......
import { Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { TWatchlistItem } from 'types/client/account';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -12,11 +10,11 @@ import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data: TWatchlistItem;
onSuccess: () => Promise<void>;
data: Pick<TWatchlistItem, 'address_hash' | 'id'>;
}
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => {
const isMobile = useIsMobile();
const fetch = useFetch();
......@@ -24,12 +22,6 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
return fetch(`/node-api/account/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data?.id, fetch ]);
const onSuccess = useCallback(async() => {
queryClient.setQueryData([ QueryKeys.watchlist ], (prevData: TWatchlist | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
}, [ data, queryClient ]);
const address = data?.address_hash;
const renderModalContent = useCallback(() => {
......
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