Commit 05964619 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Add link to advanced filter page filtered by current address and token transfer types (#2665)

* Add link to advanced filter page filtered by current address and token transfer types

* fix ts

* fix tests
parent 479fe68c
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import { ADVANCED_FILTER_TYPES } from 'types/api/advancedFilter';
import type { TokenType } from 'types/api/token';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import { Link } from 'toolkit/chakra/link';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
isLoading?: boolean;
address: string;
typeFilter: Array<TokenType>;
directionFilter: AddressFromToFilter;
}
const AddressAdvancedFilterLink = ({ isLoading, address, typeFilter, directionFilter }: Props) => {
const isInitialLoading = useIsInitialLoading(isLoading);
if (!config.features.advancedFilter.isEnabled) {
return null;
}
const queryParams = {
to_address_hashes_to_include: !directionFilter || directionFilter === 'to' ? [ address ] : undefined,
from_address_hashes_to_include: !directionFilter || directionFilter === 'from' ? [ address ] : undefined,
transaction_types: typeFilter.length > 0 ? typeFilter : ADVANCED_FILTER_TYPES.filter((type) => type !== 'coin_transfer'),
};
return (
<Link
whiteSpace="nowrap"
href={ route({ pathname: '/advanced-filter', query: queryParams }) }
flexShrink={ 0 }
loading={ isInitialLoading }
minW={ 8 }
justifyContent="center"
>
<IconSvg name="filter" boxSize={ 6 }/>
<chakra.span ml={ 1 } hideBelow="lg">Advanced filter</chakra.span>
</Link>
);
};
export default React.memo(AddressAdvancedFilterLink);
import { chakra, Flex } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { CsvExportParams } from 'types/client/address';
......@@ -9,7 +9,6 @@ import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
......@@ -28,15 +27,6 @@ const AddressCsvExportLink = ({ className, address, params, isLoading }: Props)
return null;
}
if (isInitialLoading) {
return (
<Flex className={ className } flexShrink={ 0 } alignItems="center">
<Skeleton loading boxSize={{ base: 8, lg: 6 }}/>
<Skeleton loading hideBelow="lg" w="112px" h={ 6 } ml={ 1 }/>
</Flex>
);
}
return (
<Tooltip disabled={ !isMobile } content="Download CSV">
<Link
......@@ -44,8 +34,11 @@ const AddressCsvExportLink = ({ className, address, params, isLoading }: Props)
whiteSpace="nowrap"
href={ route({ pathname: '/csv-export', query: { ...params, address } }) }
flexShrink={ 0 }
loading={ isInitialLoading }
minW={ 8 }
justifyContent="center"
>
<IconSvg name="files/csv" boxSize={{ base: '30px', lg: 6 }}/>
<IconSvg name="files/csv" boxSize={ 6 }/>
<chakra.span ml={ 1 } hideBelow="lg">Download CSV</chakra.span>
</Link>
</Tooltip>
......
......@@ -8,10 +8,9 @@ import { test, expect, devices } from 'playwright/lib';
import AddressTokenTransfers from './AddressTokenTransfers';
const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const TOKEN_HASH = '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { hash: CURRENT_ADDRESS, token: TOKEN_HASH },
query: { hash: CURRENT_ADDRESS },
},
};
......@@ -28,10 +27,10 @@ const tokenTransfersWoPagination = {
next_page_params: null,
};
test('with token filter and pagination', async({ render, mockApiResponse }) => {
test('with pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
queryParams: { type: [] },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
......@@ -42,10 +41,10 @@ test('with token filter and pagination', async({ render, mockApiResponse }) => {
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ render, mockApiResponse }) => {
test('without pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
queryParams: { type: [] },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
......@@ -59,10 +58,10 @@ test('with token filter and no pagination', async({ render, mockApiResponse }) =
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with token filter and pagination', async({ render, mockApiResponse }) => {
test('with pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
queryParams: { type: [] },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
......@@ -73,10 +72,10 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ render, mockApiResponse }) => {
test('without pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
queryParams: { type: [] },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
......
import { Box, Flex, Text } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -12,7 +12,6 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -22,16 +21,14 @@ import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import ResetIconButton from 'ui/shared/ResetIconButton';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import AddressAdvancedFilterLink from './AddressAdvancedFilterLink';
import AddressCsvExportLink from './AddressCsvExportLink';
type Filters = {
......@@ -72,7 +69,6 @@ type Props = {
const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const isMounted = useIsMounted();
const currentAddress = getQueryParamString(router.query.hash);
......@@ -80,8 +76,6 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = getQueryParamString(router.query.token) || undefined;
const [ filters, setFilters ] = React.useState<Filters>(
{
type: getTokenFilterValue(router.query.type) || [],
......@@ -92,7 +86,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
const { isError, isPlaceholderData, data, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'address_token_transfers',
pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters,
filters,
options: {
enabled: isQueryEnabled,
placeholderData: getTokenTransfersStub(undefined, {
......@@ -114,10 +108,6 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]);
const resetTokenFilter = React.useCallback(() => {
onFilterChange({});
}, [ onFilterChange ]);
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert('');
......@@ -173,7 +163,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
topic: `addresses:${ currentAddress.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
isDisabled: pagination.page !== 1,
});
useSocketMessage({
......@@ -182,20 +172,12 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
handler: handleNewSocketMessage,
});
const tokenData = React.useMemo(() => ({
address: tokenFilter || '',
name: '',
icon_url: '',
symbol: '',
type: 'ERC-20' as const,
}), [ tokenFilter ]);
if (!isMounted || !shouldRender) {
return null;
}
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const isActionBarHidden = !numActiveFilters && !data?.items.length && !currentAddress;
const content = data?.items ? (
<>
......@@ -206,14 +188,14 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
showTxInfo
top={ isActionBarHidden ? 0 : ACTION_BAR_HEIGHT_DESKTOP }
enableTimeIncrement
showSocketInfo={ pagination.page === 1 && !tokenFilter }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
isLoading={ isPlaceholderData }
/>
</Box>
<Box hideFrom="lg">
{ pagination.page === 1 && !tokenFilter && (
{ pagination.page === 1 && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ newItemsCount }
......@@ -233,24 +215,8 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
</>
) : null;
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }>
<Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by token</Text>
<Flex alignItems="center" py={ 1 }>
<TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<ResetIconButton onClick={ resetTokenFilter }/>
</Flex>
</Flex>
);
const actionBar = (
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
const actionBar = !isActionBarHidden ? (
<ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
......@@ -260,20 +226,22 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
defaultAddressFilter={ filters.filter }
isLoading={ isPlaceholderData }
/>
) }
{ currentAddress && (
<Flex columnGap={{ base: 2, lg: 6 }} ml={{ base: 2, lg: 'auto' }} _empty={{ display: 'none' }}>
<AddressAdvancedFilterLink
isLoading={ isPlaceholderData }
address={ currentAddress }
typeFilter={ filters.type }
directionFilter={ filters.filter }
/>
<AddressCsvExportLink
address={ currentAddress }
params={{ type: 'token-transfers', filterType: 'address', filterValue: filters.filter }}
ml={{ base: 2, lg: 'auto' }}
isLoading={ isPlaceholderData }
/>
) }
</Flex>
<Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/>
</ActionBar>
) }
</>
);
) : null;
return (
<DataListDisplay
......
......@@ -91,6 +91,7 @@ const NameDomainsActionBar = ({
variant="link"
onClick={ handleProtocolReset }
disabled={ protocolsFilterValue.length === 0 }
textStyle="sm"
>
Reset
</Button>
......
......@@ -99,11 +99,12 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, page, createSocket }) => {
test.slow();
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await component.getByText('100 ARIA').waitFor({ state: 'visible' });
await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 });
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -112,6 +113,7 @@ test.describe('mobile', () => {
});
test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => {
test.slow();
await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } });
await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg');
......@@ -119,7 +121,7 @@ test.describe('mobile', () => {
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await component.getByText('100 ARIA').waitFor({ state: 'visible' });
await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 });
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
......@@ -21,11 +21,12 @@ interface Props {
className?: string;
isLoading?: boolean;
tokenHash?: string;
tokenSymbol?: string;
truncation?: EntityProps['truncation'];
noIcon?: boolean;
}
const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', noIcon }: Props) => {
const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', tokenSymbol = '', noIcon }: Props) => {
const mode = useBreakpointValue(
{
base: (typeof modeProp === 'object' && 'base' in modeProp ? modeProp.base : modeProp),
......@@ -34,7 +35,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
},
) ?? 'long';
const Entity = tokenHash ? AddressEntityWithTokenFilter : AddressEntity;
const Entity = tokenHash && tokenSymbol ? AddressEntityWithTokenFilter : AddressEntity;
if (mode === 'compact') {
return (
......@@ -52,6 +53,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === from.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
tokenSymbol={ tokenSymbol }
truncation="constant"
maxW="calc(100% - 28px)"
w="min-content"
......@@ -65,6 +67,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === to.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
tokenSymbol={ tokenSymbol }
truncation="constant"
maxW="calc(100% - 28px)"
w="min-content"
......@@ -87,6 +90,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ isOutgoing }
noIcon={ noIcon }
tokenHash={ tokenHash }
tokenSymbol={ tokenSymbol }
truncation="constant"
mr={ isOutgoing ? 4 : 2 }
/>
......@@ -102,6 +106,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === to.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
tokenSymbol={ tokenSymbol }
truncation="constant"
ml={ 3 }
/>
......
......@@ -3,21 +3,29 @@ import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as AddressEntity from './AddressEntity';
interface Props extends AddressEntity.EntityProps {
tokenHash: string;
tokenSymbol: string;
}
const AddressEntityWithTokenFilter = (props: Props) => {
if (!config.features.advancedFilter.isEnabled) {
return <AddressEntity.default { ...props }/>;
}
const defaultHref = route({
pathname: '/address/[hash]',
pathname: '/advanced-filter',
query: {
...props.query,
hash: props.address.hash,
tab: 'token_transfers',
token: props.tokenHash,
scroll_to_tabs: 'true',
to_address_hashes_to_include: [ props.address.hash ],
from_address_hashes_to_include: [ props.address.hash ],
token_contract_address_hashes_to_include: [ props.tokenHash ],
token_contract_symbols_to_include: [ props.tokenSymbol ],
},
});
......
......@@ -3,6 +3,8 @@ import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as TokenEntity from './TokenEntity';
interface Props extends TokenEntity.EntityProps {
......@@ -10,14 +12,19 @@ interface Props extends TokenEntity.EntityProps {
}
const TokenEntityWithAddressFilter = (props: Props) => {
if (!config.features.advancedFilter.isEnabled) {
return <TokenEntity.default { ...props }/>;
}
const defaultHref = route({
pathname: '/address/[hash]',
pathname: '/advanced-filter',
query: {
...props.query,
hash: props.addressHash,
tab: 'token_transfers',
token: props.token.address,
scroll_to_tabs: 'true',
to_address_hashes_to_include: [ props.addressHash ],
from_address_hashes_to_include: [ props.addressHash ],
token_contract_address_hashes_to_include: [ props.token.address ],
token_contract_symbols_to_include: [ props.token.symbol ?? '' ],
},
});
......
......@@ -30,12 +30,13 @@ const TokenTypeFilter = <T extends TokenType | NFTTokenType>({ nftOnly, onChange
return (
<>
<Flex justifyContent="space-between" fontSize="sm">
<Flex justifyContent="space-between" textStyle="sm">
<Text fontWeight={ 600 } color="text.secondary">Type</Text>
<Button
variant="link"
onClick={ handleReset }
disabled={ value.length === 0 }
textStyle="sm"
>
Reset
</Button>
......
......@@ -62,6 +62,7 @@ const TokenTransferListItem = ({
to={ to }
isLoading={ isLoading }
tokenHash={ token?.address }
tokenSymbol={ token?.symbol ?? undefined }
w="100%"
fontWeight="500"
/>
......
......@@ -73,6 +73,7 @@ const TokenTransferTableItem = ({
mt="5px"
mode={{ lg: 'compact', xl: 'long' }}
tokenHash={ token?.address }
tokenSymbol={ token?.symbol ?? undefined }
/>
</TableCell>
{ (token && NFT_TOKEN_TYPE_IDS.includes(token.type)) && (
......
......@@ -34,12 +34,13 @@ const TokensBridgedChainsFilter = ({ onChange, defaultValue }: Props) => {
return (
<>
<Flex justifyContent="space-between" fontSize="sm">
<Flex justifyContent="space-between" textStyle="sm">
<Text fontWeight={ 600 } color="text.secondary">Show bridged tokens from</Text>
<Button
variant="link"
onClick={ handleReset }
disabled={ value.length === 0 }
textStyle="sm"
>
Reset
</Button>
......
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