Commit 835c9a54 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into address-entity

parents 2b2aac2f 1bd65cf3
......@@ -14,6 +14,7 @@ import type {
import type {
Address,
AddressCounters,
AddressTabsCounters,
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse,
......@@ -248,6 +249,10 @@ export const RESOURCES = {
path: '/api/v2/addresses/:hash/counters',
pathParams: [ 'hash' as const ],
},
address_tabs_counters: {
path: '/api/v2/addresses/:hash/tabs-counters',
pathParams: [ 'hash' as const ],
},
// this resource doesn't have pagination, so causing huge problems on some addresses page
// address_token_balances: {
// path: '/api/v2/addresses/:hash/token-balances',
......@@ -581,6 +586,7 @@ Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_tabs_counters' ? AddressTabsCounters :
Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
......
import xss from 'xss';
import escapeRegExp from 'lib/escapeRegExp';
export default function highlightText(text: string, query: string) {
const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
return text.replace(regex, '<mark>$1</mark>');
return xss(text.replace(regex, '<mark>$1</mark>'));
}
......@@ -27,9 +27,7 @@ export default function useAddOrSwitchChain() {
// This error code indicates that the chain has not been added to Wallet.
if (code === 4902) {
const params = {
method: 'wallet_addEthereumChain',
params: [ {
const params = [ {
chainId: hexadecimalChainId,
chainName: config.chain.name,
nativeCurrency: {
......@@ -39,14 +37,13 @@ export default function useAddOrSwitchChain() {
},
rpcUrls: [ config.chain.rpcUrl ],
blockExplorerUrls: [ config.app.baseUrl ],
} ],
} ] as never;
// in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
return await provider.request({
method: 'wallet_addEthereumChain',
params,
params: params,
});
}
......
import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTokenBalance } from 'types/api/address';
import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTabsCounters, AddressTokenBalance } from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
......@@ -42,6 +42,17 @@ export const ADDRESS_COUNTERS: AddressCounters = {
validations_count: '0',
};
export const ADDRESS_TABS_COUNTERS: AddressTabsCounters = {
coin_balances_count: 10,
internal_txs_count: 10,
logs_count: 10,
token_balances_count: 10,
token_transfers_count: 10,
transactions_count: 10,
validations_count: 10,
withdrawals_count: 10,
};
export const TOP_ADDRESS: AddressesItem = {
coin_balance: '11886682377162664596540805',
tx_count: '1835',
......
......@@ -12,6 +12,7 @@ export interface Address extends UserTags {
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
// TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
......@@ -149,3 +150,14 @@ export type AddressWithdrawalsItem = {
timestamp: string;
validator_index: number;
}
export type AddressTabsCounters = {
coin_balances_count: number;
internal_txs_count: number;
logs_count: number;
token_balances_count: number;
token_transfers_count: number;
transactions_count: number;
validations_count: number;
withdrawals_count: number;
}
......@@ -12,7 +12,7 @@ import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO } from 'stubs/address';
import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
......@@ -54,24 +54,71 @@ const AddressPageContent = () => {
},
});
const addressTabsCountersQuery = useApiQuery('address_tabs_counters', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_TABS_COUNTERS,
},
});
const contractTabs = useContractTabs(addressQuery.data);
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
config.features.beaconChain.isEnabled && addressQuery.data?.has_beacon_chain_withdrawals ?
{ id: 'withdrawals', title: 'Withdrawals', component: <AddressWithdrawals scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_token_transfers ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
{
id: 'txs',
title: 'Transactions',
count: addressTabsCountersQuery.data?.transactions_count,
component: <AddressTxs scrollRef={ tabsScrollRef }/>,
},
config.features.beaconChain.isEnabled ?
{
id: 'withdrawals',
title: 'Withdrawals',
count: addressTabsCountersQuery.data?.withdrawals_count,
component: <AddressWithdrawals scrollRef={ tabsScrollRef }/>,
} :
undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: <AddressTokens/>, subTabs: TOKEN_TABS } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
addressQuery.data?.has_validated_blocks ?
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } :
{
id: 'token_transfers',
title: 'Token transfers',
count: addressTabsCountersQuery.data?.token_transfers_count,
component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/>,
},
{
id: 'tokens',
title: 'Tokens',
count: addressTabsCountersQuery.data?.token_balances_count,
component: <AddressTokens/>,
subTabs: TOKEN_TABS,
},
{
id: 'internal_txns',
title: 'Internal txns',
count: addressTabsCountersQuery.data?.internal_txs_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef }/>,
},
{
id: 'coin_balance_history',
title: 'Coin balance history',
count: addressTabsCountersQuery.data?.coin_balances_count,
component: <AddressCoinBalance/>,
},
config.chain.verificationType === 'validation' ?
{
id: 'blocks_validated',
title: 'Blocks validated',
count: addressTabsCountersQuery.data?.validations_count,
component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/>,
} :
undefined,
addressQuery.data?.has_logs ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
{
id: 'logs',
title: 'Logs',
count: addressTabsCountersQuery.data?.logs_count,
component: <AddressLogs scrollRef={ tabsScrollRef }/>,
},
addressQuery.data?.is_contract ? {
id: 'contract',
title: () => {
......@@ -90,7 +137,7 @@ const AddressPageContent = () => {
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(Boolean);
}, [ addressQuery.data, contractTabs ]);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data ]);
const tags = (
<EntityTags
......@@ -134,7 +181,7 @@ const AddressPageContent = () => {
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : content }
{ (addressQuery.isPlaceholderData || addressTabsCountersQuery.isPlaceholderData) ? <TabsSkeleton tabs={ tabs }/> : content }
</>
);
};
......
import { Flex, Grid, Icon, Image, Box, Text, Skeleton, useColorMode } from '@chakra-ui/react';
import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/api/search';
......@@ -265,7 +266,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'contract':
case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return data.name ? <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/> : null;
return data.name ? <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(data.name) : highlightText(data.name, searchTerm) }}/> : null;
}
default:
......
import { Tr, Td, Text, Flex, Icon, Image, Box, Skeleton, useColorMode } from '@chakra-ui/react';
import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/api/search';
......@@ -123,7 +124,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
</AddressEntity.Container>
</Td>
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/>
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(data.name) : highlightText(data.name, searchTerm) }}/>
</Td>
</>
);
......
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const feature = config.features.adsBanner;
interface Props {
isLoading?: boolean;
}
......@@ -14,7 +17,7 @@ const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie) {
if (!feature.isEnabled || hasAdblockCookie) {
return null;
}
......
import type { SystemStyleObject } from '@chakra-ui/react';
import { Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const COUNTER_OVERLOAD = 50;
type Props = {
count?: number;
parentClassName: string;
}
const TasCounter = ({ count, parentClassName }: Props) => {
const zeroCountColor = useColorModeValue('blackAlpha.400', 'whiteAlpha.400');
if (count === undefined) {
return null;
}
const sx: SystemStyleObject = {
[`.${ parentClassName }:hover &`]: { color: 'inherit' },
};
return (
<Text
color={ count > 0 ? 'text_secondary' : zeroCountColor }
ml={ 1 }
sx={ sx }
transitionProperty="color"
transitionDuration="normal"
transitionTimingFunction="ease"
>
{ count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count }
</Text>
);
};
export default TasCounter;
......@@ -12,8 +12,11 @@ import React from 'react';
import type { MenuButton, TabItem } from './types';
import TabCounter from './TabCounter';
import { menuButton } from './utils';
const BUTTON_CLASSNAME = 'button-item';
interface Props {
tabs: Array<TabItem | MenuButton>;
activeTab?: TabItem;
......@@ -59,8 +62,10 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act
isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left"
data-index={ index }
className={ BUTTON_CLASSNAME }
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ BUTTON_CLASSNAME }/>
</Button>
)) }
</PopoverBody>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { TabItem } from './types';
import TestApp from 'playwright/TestApp';
import TabsWithScroll from './TabsWithScroll';
test('with counters', async({ mount }) => {
const tabs: Array<TabItem> = [
{
id: 'tab1',
title: 'First tab',
count: 11,
component: null,
},
{
id: 'tab2',
title: 'Second tab',
count: 0,
component: null,
},
{
id: 'tab3',
title: 'Third tab',
count: 51,
component: null,
},
];
const component = await mount(
<TestApp>
<TabsWithScroll tabs={ tabs }/>
</TestApp>,
);
await component.getByText('Third tab').hover();
await expect(component).toHaveScreenshot();
});
......@@ -19,9 +19,12 @@ import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import TabCounter from './TabCounter';
import TabsMenu from './TabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
const TAB_CLASSNAME = 'tab-item';
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
......@@ -167,8 +170,10 @@ const TabsWithScroll = ({
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
flexShrink={ 0 }
className={ TAB_CLASSNAME }
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ TAB_CLASSNAME }/>
</Tab>
);
}) }
......
......@@ -3,6 +3,7 @@ import type React from 'react';
export interface TabItem {
id: string;
title: string | (() => React.ReactNode);
count?: number;
component: React.ReactNode;
}
......@@ -13,5 +14,6 @@ export type RoutedSubTab = Omit<TabItem, 'subTabs'>;
export interface MenuButton {
id: null;
title: string;
count?: never;
component: null;
}
......@@ -6486,6 +6486,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssfilter@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
integrity sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==
csso@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
......@@ -12938,6 +12943,14 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xss@^1.0.14:
version "1.0.14"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694"
integrity sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==
dependencies:
commander "^2.20.3"
cssfilter "0.0.10"
xtend@^4.0.0, xtend@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
......
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