Commit 57b54a6c authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1823 from blockscout/address-tabs-loading

Address tabs loading
parents ab3e070a bbec6bcd
import React from 'react';
export default function useIsMounted() {
const [ isMounted, setIsMounted ] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
}, [ ]);
return isMounted;
}
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
...@@ -11,7 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi'; ...@@ -11,7 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app'; import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
const Address = dynamic(() => import('ui/pages/Address'), { ssr: false }); import Address from 'ui/pages/Address';
const pathname: Route['pathname'] = '/address/[hash]'; const pathname: Route['pathname'] = '/address/[hash]';
......
...@@ -7,6 +7,7 @@ import type { NovesHistoryFilterValue } from 'types/api/noves'; ...@@ -7,6 +7,7 @@ import type { NovesHistoryFilterValue } from 'types/api/noves';
import { NovesHistoryFilterValues } from 'types/api/noves'; import { NovesHistoryFilterValues } from 'types/api/noves';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -25,10 +26,12 @@ const getFilterValue = (getFilterValueFromQuery<NovesHistoryFilterValue>).bind(n ...@@ -25,10 +26,12 @@ const getFilterValue = (getFilterValueFromQuery<NovesHistoryFilterValue>).bind(n
type Props = { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
} }
const AddressAccountHistory = ({ scrollRef }: Props) => { const AddressAccountHistory = ({ scrollRef, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); const currentAddress = getQueryParamString(router.query.hash).toLowerCase();
...@@ -49,6 +52,10 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { ...@@ -49,6 +52,10 @@ const AddressAccountHistory = ({ scrollRef }: Props) => {
setFilterValue(newVal); setFilterValue(newVal);
}, [ ]); }, [ ]);
if (!isMounted || !shouldRender) {
return null;
}
const actionBar = ( const actionBar = (
<ActionBar mt={ -6 } pb={{ base: 6, md: 5 }}> <ActionBar mt={ -6 } pb={{ base: 6, md: 5 }}>
<AccountHistoryFilter <AccountHistoryFilter
......
...@@ -8,6 +8,7 @@ import type { AddressBlocksValidatedResponse } from 'types/api/address'; ...@@ -8,6 +8,7 @@ import type { AddressBlocksValidatedResponse } from 'types/api/address';
import config from 'configs/app'; import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMounted from 'lib/hooks/useIsMounted';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
...@@ -25,12 +26,14 @@ import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksVali ...@@ -25,12 +26,14 @@ import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksVali
interface Props { interface Props {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
} }
const AddressBlocksValidated = ({ scrollRef }: Props) => { const AddressBlocksValidated = ({ scrollRef, shouldRender = true }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const addressHash = String(router.query.hash); const addressHash = String(router.query.hash);
const query = useQueryWithPages({ const query = useQueryWithPages({
...@@ -84,6 +87,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -84,6 +87,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
handler: handleNewSocketMessage, handler: handleNewSocketMessage,
}); });
if (!isMounted || !shouldRender) {
return null;
}
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
{ socketAlert && <SocketAlert mb={ 6 }/> } { socketAlert && <SocketAlert mb={ 6 }/> }
......
...@@ -6,6 +6,7 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -6,6 +6,7 @@ import type { SocketMessage } from 'lib/socket/types';
import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -17,10 +18,16 @@ import SocketAlert from 'ui/shared/SocketAlert'; ...@@ -17,10 +18,16 @@ import SocketAlert from 'ui/shared/SocketAlert';
import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart';
import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory'; import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory';
const AddressCoinBalance = () => { type Props = {
shouldRender?: boolean;
}
const AddressCoinBalance = ({ shouldRender = true }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const addressHash = getQueryParamString(router.query.hash); const addressHash = getQueryParamString(router.query.hash);
...@@ -78,6 +85,10 @@ const AddressCoinBalance = () => { ...@@ -78,6 +85,10 @@ const AddressCoinBalance = () => {
handler: handleNewSocketMessage, handler: handleNewSocketMessage,
}); });
if (!isMounted || !shouldRender) {
return null;
}
return ( return (
<> <>
{ socketAlert && <SocketAlert mb={ 6 }/> } { socketAlert && <SocketAlert mb={ 6 }/> }
......
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
...@@ -60,6 +61,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -60,6 +61,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
has_validated_blocks: false, has_validated_blocks: false,
}), [ addressHash ]); }), [ addressHash ]);
const isMounted = useIsMounted();
// error handling (except 404 codes) // error handling (except 404 codes)
if (addressQuery.isError) { if (addressQuery.isError) {
if (isCustomAppError(addressQuery.error)) { if (isCustomAppError(addressQuery.error)) {
...@@ -74,7 +77,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -74,7 +77,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const data = addressQuery.isError ? error404Data : addressQuery.data; const data = addressQuery.isError ? error404Data : addressQuery.data;
if (!data) { if (!data || !isMounted) {
return null; return null;
} }
......
...@@ -6,6 +6,7 @@ import type { AddressFromToFilter } from 'types/api/address'; ...@@ -6,6 +6,7 @@ import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMounted from 'lib/hooks/useIsMounted';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { INTERNAL_TX } from 'stubs/internalTx'; import { INTERNAL_TX } from 'stubs/internalTx';
...@@ -22,8 +23,14 @@ import AddressIntTxsList from './internals/AddressIntTxsList'; ...@@ -22,8 +23,14 @@ import AddressIntTxsList from './internals/AddressIntTxsList';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
}
const AddressInternalTxs = ({ scrollRef, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -55,6 +62,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -55,6 +62,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange({ filter: newVal }); onFilterChange({ filter: newVal });
}, [ onFilterChange ]); }, [ onFilterChange ]);
if (!isMounted || !shouldRender) {
return null;
}
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { LOG } from 'stubs/log'; import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -12,8 +13,14 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; ...@@ -12,8 +13,14 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressCsvExportLink from './AddressCsvExportLink'; import AddressCsvExportLink from './AddressCsvExportLink';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props ={
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
}
const AddressLogs = ({ scrollRef, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
...@@ -41,6 +48,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -41,6 +48,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
</ActionBar> </ActionBar>
); );
if (!isMounted || !shouldRender) {
return null;
}
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address" isLoading={ isPlaceholderData }/>) : null; const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address" isLoading={ isPlaceholderData }/>) : null;
return ( return (
......
...@@ -13,6 +13,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -13,6 +13,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
...@@ -63,14 +64,16 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -63,14 +64,16 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
type Props = { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
// for tests only // for tests only
overloadCount?: number; overloadCount?: number;
} }
const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMounted = useIsMounted();
const currentAddress = getQueryParamString(router.query.hash); const currentAddress = getQueryParamString(router.query.hash);
...@@ -179,6 +182,18 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -179,6 +182,18 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
handler: handleNewSocketMessage, 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 numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
...@@ -218,14 +233,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -218,14 +233,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
</> </>
) : null; ) : null;
const tokenData = React.useMemo(() => ({
address: tokenFilter || '',
name: '',
icon_url: '',
symbol: '',
type: 'ERC-20' as const,
}), [ tokenFilter ]);
const tokenFilterComponent = tokenFilter && ( const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }> <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> <Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by token</Text>
......
...@@ -9,6 +9,7 @@ import { useAppContext } from 'lib/contexts/app'; ...@@ -9,6 +9,7 @@ import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address';
...@@ -41,9 +42,14 @@ const TAB_LIST_PROPS_MOBILE = { ...@@ -41,9 +42,14 @@ const TAB_LIST_PROPS_MOBILE = {
const getTokenFilterValue = (getFilterValuesFromQuery<NFTTokenType>).bind(null, NFT_TOKEN_TYPE_IDS); const getTokenFilterValue = (getFilterValuesFromQuery<NFTTokenType>).bind(null, NFT_TOKEN_TYPE_IDS);
const AddressTokens = () => { type Props = {
shouldRender?: boolean;
}
const AddressTokens = ({ shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMounted = useIsMounted();
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
...@@ -99,6 +105,10 @@ const AddressTokens = () => { ...@@ -99,6 +105,10 @@ const AddressTokens = () => {
setTokenTypes(value); setTokenTypes(value);
}, [ nftsQuery, collectionsQuery ]); }, [ nftsQuery, collectionsQuery ]);
if (!isMounted || !shouldRender) {
return null;
}
const nftTypeFilter = ( const nftTypeFilter = (
<PopoverFilter isActive={ tokenTypes && tokenTypes.length > 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }> <PopoverFilter isActive={ tokenTypes && tokenTypes.length > 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }>
<TokenTypeFilter<NFTTokenType> nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/> <TokenTypeFilter<NFTTokenType> nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/>
......
...@@ -10,6 +10,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue, T ...@@ -10,6 +10,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue, T
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -47,13 +48,15 @@ const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, ...@@ -47,13 +48,15 @@ const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction,
type Props = { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
// for tests only // for tests only
overloadCount?: number; overloadCount?: number;
} }
const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMounted = useIsMounted();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
...@@ -156,6 +159,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -156,6 +159,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
handler: handleNewSocketMessage, handler: handleNewSocketMessage,
}); });
if (!isMounted || !shouldRender) {
return null;
}
const filter = ( const filter = (
<AddressTxsFilter <AddressTxsFilter
defaultFilter={ filterValue } defaultFilter={ filterValue }
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OPS_ITEM } from 'stubs/userOps'; import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -9,10 +10,12 @@ import UserOpsContent from 'ui/userOps/UserOpsContent'; ...@@ -9,10 +10,12 @@ import UserOpsContent from 'ui/userOps/UserOpsContent';
type Props = { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
} }
const AddressUserOps = ({ scrollRef }: Props) => { const AddressUserOps = ({ scrollRef, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -29,6 +32,10 @@ const AddressUserOps = ({ scrollRef }: Props) => { ...@@ -29,6 +32,10 @@ const AddressUserOps = ({ scrollRef }: Props) => {
filters: { sender: hash }, filters: { sender: hash },
}); });
if (!isMounted || !shouldRender) {
return null;
}
return <UserOpsContent query={ userOpsQuery } showSender={ false }/>; return <UserOpsContent query={ userOpsQuery } showSender={ false }/>;
}; };
......
...@@ -2,6 +2,7 @@ import { Show, Hide } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Show, Hide } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals'; import { WITHDRAWAL } from 'stubs/withdrawals';
...@@ -12,8 +13,13 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; ...@@ -12,8 +13,13 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem'; import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem';
import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable'; import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable';
const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
}
const AddressWithdrawals = ({ scrollRef, shouldRender = true }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -28,6 +34,11 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -28,6 +34,11 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
} }), } }),
}, },
}); });
if (!isMounted || !shouldRender) {
return null;
}
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
......
...@@ -39,7 +39,6 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -39,7 +39,6 @@ import IconSvg from 'ui/shared/IconSvg';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ]; const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
...@@ -75,19 +74,22 @@ const AddressPageContent = () => { ...@@ -75,19 +74,22 @@ const AddressPageContent = () => {
const contractTabs = useContractTabs(addressQuery.data); const contractTabs = useContractTabs(addressQuery.data);
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData;
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
{ {
id: 'txs', id: 'txs',
title: 'Transactions', title: 'Transactions',
count: addressTabsCountersQuery.data?.transactions_count, count: addressTabsCountersQuery.data?.transactions_count,
component: <AddressTxs scrollRef={ tabsScrollRef }/>, component: <AddressTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
}, },
txInterpretation.isEnabled && txInterpretation.provider === 'noves' ? txInterpretation.isEnabled && txInterpretation.provider === 'noves' ?
{ {
id: 'account_history', id: 'account_history',
title: 'Account history', title: 'Account history',
component: <AddressAccountHistory scrollRef={ tabsScrollRef }/>, component: <AddressAccountHistory scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
} : } :
undefined, undefined,
config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ? config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ?
...@@ -95,7 +97,7 @@ const AddressPageContent = () => { ...@@ -95,7 +97,7 @@ const AddressPageContent = () => {
id: 'user_ops', id: 'user_ops',
title: 'User operations', title: 'User operations',
count: userOpsAccountQuery.data?.total_ops, count: userOpsAccountQuery.data?.total_ops,
component: <AddressUserOps/>, component: <AddressUserOps shouldRender={ !isTabsLoading }/>,
} : } :
undefined, undefined,
config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ?
...@@ -103,39 +105,39 @@ const AddressPageContent = () => { ...@@ -103,39 +105,39 @@ const AddressPageContent = () => {
id: 'withdrawals', id: 'withdrawals',
title: 'Withdrawals', title: 'Withdrawals',
count: addressTabsCountersQuery.data?.withdrawals_count, count: addressTabsCountersQuery.data?.withdrawals_count,
component: <AddressWithdrawals scrollRef={ tabsScrollRef }/>, component: <AddressWithdrawals scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
} : } :
undefined, undefined,
{ {
id: 'token_transfers', id: 'token_transfers',
title: 'Token transfers', title: 'Token transfers',
count: addressTabsCountersQuery.data?.token_transfers_count, count: addressTabsCountersQuery.data?.token_transfers_count,
component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/>, component: <AddressTokenTransfers scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
}, },
{ {
id: 'tokens', id: 'tokens',
title: 'Tokens', title: 'Tokens',
count: addressTabsCountersQuery.data?.token_balances_count, count: addressTabsCountersQuery.data?.token_balances_count,
component: <AddressTokens/>, component: <AddressTokens shouldRender={ !isTabsLoading }/>,
subTabs: TOKEN_TABS, subTabs: TOKEN_TABS,
}, },
{ {
id: 'internal_txns', id: 'internal_txns',
title: 'Internal txns', title: 'Internal txns',
count: addressTabsCountersQuery.data?.internal_txs_count, count: addressTabsCountersQuery.data?.internal_txs_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef }/>, component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
}, },
{ {
id: 'coin_balance_history', id: 'coin_balance_history',
title: 'Coin balance history', title: 'Coin balance history',
component: <AddressCoinBalance/>, component: <AddressCoinBalance shouldRender={ !isTabsLoading }/>,
}, },
config.chain.verificationType === 'validation' && addressTabsCountersQuery.data?.validations_count ? config.chain.verificationType === 'validation' && addressTabsCountersQuery.data?.validations_count ?
{ {
id: 'blocks_validated', id: 'blocks_validated',
title: 'Blocks validated', title: 'Blocks validated',
count: addressTabsCountersQuery.data?.validations_count, count: addressTabsCountersQuery.data?.validations_count,
component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/>, component: <AddressBlocksValidated scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
} : } :
undefined, undefined,
addressTabsCountersQuery.data?.logs_count ? addressTabsCountersQuery.data?.logs_count ?
...@@ -143,9 +145,10 @@ const AddressPageContent = () => { ...@@ -143,9 +145,10 @@ const AddressPageContent = () => {
id: 'logs', id: 'logs',
title: 'Logs', title: 'Logs',
count: addressTabsCountersQuery.data?.logs_count, count: addressTabsCountersQuery.data?.logs_count,
component: <AddressLogs scrollRef={ tabsScrollRef }/>, component: <AddressLogs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading }/>,
} : } :
undefined, undefined,
addressQuery.data?.is_contract ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: () => { title: () => {
...@@ -164,9 +167,7 @@ const AddressPageContent = () => { ...@@ -164,9 +167,7 @@ const AddressPageContent = () => {
subTabs: contractTabs.map(tab => tab.id), subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data ]); }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]);
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
const tags = ( const tags = (
<EntityTags <EntityTags
...@@ -183,7 +184,9 @@ const AddressPageContent = () => { ...@@ -183,7 +184,9 @@ const AddressPageContent = () => {
/> />
); );
const content = (addressQuery.isError || addressQuery.isDegradedData) ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>; const content = (addressQuery.isError || addressQuery.isDegradedData) ?
null :
<RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }} isLoading={ isTabsLoading }/>;
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
...@@ -250,10 +253,7 @@ const AddressPageContent = () => { ...@@ -250,10 +253,7 @@ const AddressPageContent = () => {
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ (isLoading || addressTabsCountersQuery.isPlaceholderData) ? { content }
<TabsSkeleton tabs={ tabs }/> :
content
}
</> </>
); );
}; };
......
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, devices, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { buildExternalAssetFilePath } from 'configs/app/utils'; import { buildExternalAssetFilePath } from 'configs/app/utils';
...@@ -53,7 +53,7 @@ const testFn: Parameters<typeof test>[1] = async({ mount, page }) => { ...@@ -53,7 +53,7 @@ const testFn: Parameters<typeof test>[1] = async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}; };
test('base view +@mobile +@dark-mode', testFn); test('base view +@dark-mode', testFn);
const testWithFeaturedApp = test.extend({ const testWithFeaturedApp = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
...@@ -63,7 +63,7 @@ const testWithFeaturedApp = test.extend({ ...@@ -63,7 +63,7 @@ const testWithFeaturedApp = test.extend({
]) as any, ]) as any,
}); });
testWithFeaturedApp('with featured app +@mobile +@dark-mode', testFn); testWithFeaturedApp('with featured app +@dark-mode', testFn);
const testWithBanner = test.extend({ const testWithBanner = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
...@@ -74,7 +74,7 @@ const testWithBanner = test.extend({ ...@@ -74,7 +74,7 @@ const testWithBanner = test.extend({
]) as any, ]) as any,
}); });
testWithBanner('with banner +@mobile +@dark-mode', testFn); testWithBanner('with banner +@dark-mode', testFn);
const testWithScoreFeature = test.extend({ const testWithScoreFeature = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
...@@ -85,7 +85,7 @@ const testWithScoreFeature = test.extend({ ...@@ -85,7 +85,7 @@ const testWithScoreFeature = test.extend({
]) as any, ]) as any,
}); });
testWithScoreFeature('with scores +@mobile +@dark-mode', async({ mount, page }) => { testWithScoreFeature('with scores +@dark-mode', async({ mount, page }) => {
await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(appsMock), body: JSON.stringify(appsMock),
...@@ -115,3 +115,44 @@ testWithScoreFeature('with scores +@mobile +@dark-mode', async({ mount, page }) ...@@ -115,3 +115,44 @@ testWithScoreFeature('with scores +@mobile +@dark-mode', async({ mount, page })
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
// I had a memory error while running tests in GH actions
// separate run for mobile tests fixes it
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', testFn);
testWithFeaturedApp('with featured app', testFn);
testWithBanner('with banner', testFn);
testWithScoreFeature('with scores', async({ mount, page }) => {
await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(appsMock),
}));
await page.route(MARKETPLACE_SECURITY_REPORTS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(securityReportsMock),
}));
await Promise.all(appsMock.map(app =>
page.route(app.logo, (route) =>
route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
}),
),
));
const component = await mount(
<TestApp>
<Marketplace/>
</TestApp>,
);
await component.getByText('Apps scores').click();
await expect(component).toHaveScreenshot();
});
});
...@@ -21,7 +21,6 @@ import type { IconName } from 'ui/shared/IconSvg'; ...@@ -21,7 +21,6 @@ import type { IconName } from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup'; import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
...@@ -94,7 +93,7 @@ const Marketplace = () => { ...@@ -94,7 +93,7 @@ const Marketplace = () => {
tabs.unshift({ tabs.unshift({
id: MarketplaceCategory.FAVORITES, id: MarketplaceCategory.FAVORITES,
title: () => <IconSvg name="star_outline" w={ 5 } h={ 5 }/>, title: () => <IconSvg name="star_outline" w={ 5 } h={ 5 } display="flex"/>,
count: null, count: null,
component: null, component: null,
}); });
...@@ -180,16 +179,13 @@ const Marketplace = () => { ...@@ -180,16 +179,13 @@ const Marketplace = () => {
/> />
<Box marginTop={{ base: 0, lg: 8 }}> <Box marginTop={{ base: 0, lg: 8 }}>
{ (isCategoriesPlaceholderData) ? (
<TabsSkeleton tabs={ categoryTabs }/>
) : (
<TabsWithScroll <TabsWithScroll
tabs={ categoryTabs } tabs={ categoryTabs }
onTabChange={ handleCategoryChange } onTabChange={ handleCategoryChange }
defaultTabIndex={ selectedCategoryIndex } defaultTabIndex={ selectedCategoryIndex }
marginBottom={ -2 } marginBottom={ -2 }
isLoading={ isCategoriesPlaceholderData }
/> />
) }
</Box> </Box>
<Flex direction={{ base: 'column', lg: 'row' }} mb={{ base: 4, lg: 6 }} gap={{ base: 4, lg: 3 }}> <Flex direction={{ base: 'column', lg: 'row' }} mb={{ base: 4, lg: 6 }} gap={{ base: 4, lg: 3 }}>
......
import type { StyleProps, ThemingProps } from '@chakra-ui/react'; import type { StyleProps, ThemingProps } from '@chakra-ui/react';
import { Box, Tab, TabList, useColorModeValue } from '@chakra-ui/react'; import { Box, Skeleton, Tab, TabList, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
...@@ -24,6 +24,7 @@ interface Props extends TabsProps { ...@@ -24,6 +24,7 @@ interface Props extends TabsProps {
activeTabIndex: number; activeTabIndex: number;
onItemClick: (index: number) => void; onItemClick: (index: number) => void;
themeProps: ThemingProps<'Tabs'>; themeProps: ThemingProps<'Tabs'>;
isLoading?: boolean;
} }
const AdaptiveTabsList = (props: Props) => { const AdaptiveTabsList = (props: Props) => {
...@@ -113,8 +114,10 @@ const AdaptiveTabsList = (props: Props) => { ...@@ -113,8 +114,10 @@ const AdaptiveTabsList = (props: Props) => {
}, },
}} }}
> >
<Skeleton isLoaded={ !props.isLoading }>
{ typeof tab.title === 'function' ? tab.title() : tab.title } { typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count }/> <TabCounter count={ tab.count }/>
</Skeleton>
</Tab> </Tab>
); );
}) } }) }
......
...@@ -17,9 +17,10 @@ interface Props extends ThemingProps<'Tabs'> { ...@@ -17,9 +17,10 @@ interface Props extends ThemingProps<'Tabs'> {
stickyEnabled?: boolean; stickyEnabled?: boolean;
className?: string; className?: string;
onTabChange?: (index: number) => void; onTabChange?: (index: number) => void;
isLoading?: boolean;
} }
const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, ...themeProps }: Props) => { const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, isLoading, ...themeProps }: Props) => {
const router = useRouter(); const router = useRouter();
const tabIndex = useTabIndexFromQuery(tabs); const tabIndex = useTabIndexFromQuery(tabs);
const tabsRef = useRef<HTMLDivElement>(null); const tabsRef = useRef<HTMLDivElement>(null);
...@@ -63,6 +64,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl ...@@ -63,6 +64,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl
stickyEnabled={ stickyEnabled } stickyEnabled={ stickyEnabled }
onTabChange={ handleTabChange } onTabChange={ handleTabChange }
defaultTabIndex={ tabIndex } defaultTabIndex={ tabIndex }
isLoading={ isLoading }
{ ...themeProps } { ...themeProps }
/> />
); );
......
...@@ -25,6 +25,7 @@ export interface Props extends ThemingProps<'Tabs'> { ...@@ -25,6 +25,7 @@ export interface Props extends ThemingProps<'Tabs'> {
stickyEnabled?: boolean; stickyEnabled?: boolean;
onTabChange?: (index: number) => void; onTabChange?: (index: number) => void;
defaultTabIndex?: number; defaultTabIndex?: number;
isLoading?: boolean;
className?: string; className?: string;
} }
...@@ -37,6 +38,7 @@ const TabsWithScroll = ({ ...@@ -37,6 +38,7 @@ const TabsWithScroll = ({
stickyEnabled, stickyEnabled,
onTabChange, onTabChange,
defaultTabIndex, defaultTabIndex,
isLoading,
className, className,
...themeProps ...themeProps
}: Props) => { }: Props) => {
...@@ -101,6 +103,7 @@ const TabsWithScroll = ({ ...@@ -101,6 +103,7 @@ const TabsWithScroll = ({
activeTabIndex={ activeTabIndex } activeTabIndex={ activeTabIndex }
onItemClick={ handleTabChange } onItemClick={ handleTabChange }
themeProps={ themeProps } themeProps={ themeProps }
isLoading={ isLoading }
/> />
<TabPanels> <TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) } { tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
......
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