Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions ui/address/AddressAdvancedFilterLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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);
17 changes: 5 additions & 12 deletions ui/address/AddressCsvExportLink.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { chakra, Flex } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';

import type { CsvExportParams } from 'types/client/address';
Expand All @@ -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';

Expand All @@ -28,24 +27,18 @@ 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
className={ className }
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>
Expand Down
19 changes: 9 additions & 10 deletions ui/address/AddressTokenTransfers.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
};

Expand All @@ -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 }}>
Expand All @@ -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 }}>
Expand All @@ -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 }}>
Expand All @@ -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 }}>
Expand Down
98 changes: 33 additions & 65 deletions ui/address/AddressTokenTransfers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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';
Expand All @@ -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';
Expand All @@ -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 = {
Expand Down Expand Up @@ -72,16 +69,13 @@ 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);

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) || [],
Expand All @@ -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, {
Expand All @@ -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('');

Expand Down Expand Up @@ -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({
Expand All @@ -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 ? (
<>
Expand All @@ -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 }
Expand All @@ -233,47 +215,33 @@ 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 }/>
const actionBar = !isActionBarHidden ? (
<ActionBar mt={ -6 }>
<TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
withAddressFilter
onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
isLoading={ isPlaceholderData }
/>
<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 }}
isLoading={ isPlaceholderData }
/>
</Flex>
</Flex>
);

const actionBar = (
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
withAddressFilter
onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
isLoading={ isPlaceholderData }
/>
) }
{ currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
params={{ type: 'token-transfers', filterType: 'address', filterValue: filters.filter }}
ml={{ base: 2, lg: 'auto' }}
isLoading={ isPlaceholderData }
/>
) }
<Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/>
</ActionBar>
) }
</>
);
<Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/>
</ActionBar>
) : null;

return (
<DataListDisplay
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ui/nameDomains/NameDomainsActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const NameDomainsActionBar = ({
variant="link"
onClick={ handleProtocolReset }
disabled={ protocolsFilterValue.length === 0 }
textStyle="sm"
>
Reset
</Button>
Expand Down
Loading
Loading