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 { ...@@ -14,6 +14,7 @@ import type {
import type { import type {
Address, Address,
AddressCounters, AddressCounters,
AddressTabsCounters,
AddressTransactionsResponse, AddressTransactionsResponse,
AddressTokenTransferResponse, AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryResponse,
...@@ -248,6 +249,10 @@ export const RESOURCES = { ...@@ -248,6 +249,10 @@ export const RESOURCES = {
path: '/api/v2/addresses/:hash/counters', path: '/api/v2/addresses/:hash/counters',
pathParams: [ 'hash' as const ], 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 // this resource doesn't have pagination, so causing huge problems on some addresses page
// address_token_balances: { // address_token_balances: {
// path: '/api/v2/addresses/:hash/token-balances', // path: '/api/v2/addresses/:hash/token-balances',
...@@ -581,6 +586,7 @@ Q extends 'tx_state_changes' ? TxStateChanges : ...@@ -581,6 +586,7 @@ Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'addresses' ? AddressesResponse : Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
Q extends 'address_tabs_counters' ? AddressTabsCounters :
Q extends 'address_txs' ? AddressTransactionsResponse : Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse : Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse : Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
......
import xss from 'xss';
import escapeRegExp from 'lib/escapeRegExp'; import escapeRegExp from 'lib/escapeRegExp';
export default function highlightText(text: string, query: string) { export default function highlightText(text: string, query: string) {
const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi'); const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
return text.replace(regex, '<mark>$1</mark>'); return xss(text.replace(regex, '<mark>$1</mark>'));
} }
...@@ -27,26 +27,23 @@ export default function useAddOrSwitchChain() { ...@@ -27,26 +27,23 @@ export default function useAddOrSwitchChain() {
// This error code indicates that the chain has not been added to Wallet. // This error code indicates that the chain has not been added to Wallet.
if (code === 4902) { if (code === 4902) {
const params = { const params = [ {
method: 'wallet_addEthereumChain', chainId: hexadecimalChainId,
params: [ { chainName: config.chain.name,
chainId: hexadecimalChainId, nativeCurrency: {
chainName: config.chain.name, name: config.chain.currency.name,
nativeCurrency: { symbol: config.chain.currency.symbol,
name: config.chain.currency.name, decimals: config.chain.currency.decimals,
symbol: config.chain.currency.symbol, },
decimals: config.chain.currency.decimals, rpcUrls: [ config.chain.rpcUrl ],
}, blockExplorerUrls: [ config.app.baseUrl ],
rpcUrls: [ config.chain.rpcUrl ], } ] as never;
blockExplorerUrls: [ config.app.baseUrl ], // in wagmi types for wallet_addEthereumChain method is not provided
} ], // eslint-disable-next-line @typescript-eslint/no-explicit-any
// 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({ return await provider.request({
method: 'wallet_addEthereumChain', method: 'wallet_addEthereumChain',
params, params: params,
}); });
} }
......
...@@ -79,7 +79,8 @@ ...@@ -79,7 +79,8 @@
"swagger-ui-react": "^5.1.0", "swagger-ui-react": "^5.1.0",
"use-font-face-observer": "^1.2.1", "use-font-face-observer": "^1.2.1",
"viem": "^1.1.8", "viem": "^1.1.8",
"wagmi": "^1.3.3" "wagmi": "^1.3.3",
"xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.35.1", "@playwright/experimental-ct-react": "1.35.1",
......
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 type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH } from './addressParams';
...@@ -42,6 +42,17 @@ export const ADDRESS_COUNTERS: AddressCounters = { ...@@ -42,6 +42,17 @@ export const ADDRESS_COUNTERS: AddressCounters = {
validations_count: '0', 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 = { export const TOP_ADDRESS: AddressesItem = {
coin_balance: '11886682377162664596540805', coin_balance: '11886682377162664596540805',
tx_count: '1835', tx_count: '1835',
......
...@@ -12,6 +12,7 @@ export interface Address extends UserTags { ...@@ -12,6 +12,7 @@ export interface Address extends UserTags {
creator_address_hash: string | null; creator_address_hash: string | null;
creation_tx_hash: string | null; creation_tx_hash: string | null;
exchange_rate: 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_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean; has_custom_methods_read: boolean;
has_custom_methods_write: boolean; has_custom_methods_write: boolean;
...@@ -149,3 +150,14 @@ export type AddressWithdrawalsItem = { ...@@ -149,3 +150,14 @@ export type AddressWithdrawalsItem = {
timestamp: string; timestamp: string;
validator_index: number; 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'; ...@@ -12,7 +12,7 @@ import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs'; import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; 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 AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
...@@ -54,24 +54,71 @@ const AddressPageContent = () => { ...@@ -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 contractTabs = useContractTabs(addressQuery.data);
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> }, {
config.features.beaconChain.isEnabled && addressQuery.data?.has_beacon_chain_withdrawals ? id: 'txs',
{ id: 'withdrawals', title: 'Withdrawals', component: <AddressWithdrawals scrollRef={ tabsScrollRef }/> } : title: 'Transactions',
undefined, count: addressTabsCountersQuery.data?.transactions_count,
addressQuery.data?.has_token_transfers ? component: <AddressTxs scrollRef={ tabsScrollRef }/>,
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } : },
config.features.beaconChain.isEnabled ?
{
id: 'withdrawals',
title: 'Withdrawals',
count: addressTabsCountersQuery.data?.withdrawals_count,
component: <AddressWithdrawals scrollRef={ tabsScrollRef }/>,
} :
undefined, 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: 'token_transfers',
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, title: 'Token transfers',
addressQuery.data?.has_validated_blocks ? count: addressTabsCountersQuery.data?.token_transfers_count,
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } : 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, 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 ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: () => { title: () => {
...@@ -90,7 +137,7 @@ const AddressPageContent = () => { ...@@ -90,7 +137,7 @@ const AddressPageContent = () => {
subTabs: contractTabs.map(tab => tab.id), subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ addressQuery.data, contractTabs ]); }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data ]);
const tags = ( const tags = (
<EntityTags <EntityTags
...@@ -134,7 +181,7 @@ const AddressPageContent = () => { ...@@ -134,7 +181,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>
{ 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 { Flex, Grid, Icon, Image, Box, Text, Skeleton, useColorMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
...@@ -265,7 +266,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -265,7 +266,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); 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: default:
......
import { Tr, Td, Text, Flex, Icon, Image, Box, Skeleton, useColorMode } from '@chakra-ui/react'; import { Tr, Td, Text, Flex, Icon, Image, Box, Skeleton, useColorMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
...@@ -123,7 +124,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -123,7 +124,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
</AddressEntity.Container> </AddressEntity.Container>
</Td> </Td>
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle"> <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> </Td>
</> </>
); );
......
import { GridItem } from '@chakra-ui/react'; import { GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const feature = config.features.adsBanner;
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
...@@ -14,7 +17,7 @@ const DetailsSponsoredItem = ({ isLoading }: Props) => { ...@@ -14,7 +17,7 @@ const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie) { if (!feature.isEnabled || hasAdblockCookie) {
return null; 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'; ...@@ -12,8 +12,11 @@ import React from 'react';
import type { MenuButton, TabItem } from './types'; import type { MenuButton, TabItem } from './types';
import TabCounter from './TabCounter';
import { menuButton } from './utils'; import { menuButton } from './utils';
const BUTTON_CLASSNAME = 'button-item';
interface Props { interface Props {
tabs: Array<TabItem | MenuButton>; tabs: Array<TabItem | MenuButton>;
activeTab?: TabItem; activeTab?: TabItem;
...@@ -59,8 +62,10 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act ...@@ -59,8 +62,10 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act
isActive={ activeTab ? activeTab.id === tab.id : false } isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left" justifyContent="left"
data-index={ index } data-index={ index }
className={ BUTTON_CLASSNAME }
> >
{ typeof tab.title === 'function' ? tab.title() : tab.title } { typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ BUTTON_CLASSNAME }/>
</Button> </Button>
)) } )) }
</PopoverBody> </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'; ...@@ -19,9 +19,12 @@ import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky'; import useIsSticky from 'lib/hooks/useIsSticky';
import TabCounter from './TabCounter';
import TabsMenu from './TabsMenu'; import TabsMenu from './TabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs'; import useAdaptiveTabs from './useAdaptiveTabs';
const TAB_CLASSNAME = 'tab-item';
const hiddenItemStyles: StyleProps = { const hiddenItemStyles: StyleProps = {
position: 'absolute', position: 'absolute',
top: '-9999px', top: '-9999px',
...@@ -167,8 +170,10 @@ const TabsWithScroll = ({ ...@@ -167,8 +170,10 @@ const TabsWithScroll = ({
{ ...(index < tabsCut ? {} : hiddenItemStyles) } { ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start" scrollSnapAlign="start"
flexShrink={ 0 } flexShrink={ 0 }
className={ TAB_CLASSNAME }
> >
{ typeof tab.title === 'function' ? tab.title() : tab.title } { typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ TAB_CLASSNAME }/>
</Tab> </Tab>
); );
}) } }) }
......
...@@ -3,6 +3,7 @@ import type React from 'react'; ...@@ -3,6 +3,7 @@ import type React from 'react';
export interface TabItem { export interface TabItem {
id: string; id: string;
title: string | (() => React.ReactNode); title: string | (() => React.ReactNode);
count?: number;
component: React.ReactNode; component: React.ReactNode;
} }
...@@ -13,5 +14,6 @@ export type RoutedSubTab = Omit<TabItem, 'subTabs'>; ...@@ -13,5 +14,6 @@ export type RoutedSubTab = Omit<TabItem, 'subTabs'>;
export interface MenuButton { export interface MenuButton {
id: null; id: null;
title: string; title: string;
count?: never;
component: null; component: null;
} }
...@@ -6486,6 +6486,11 @@ cssesc@^3.0.0: ...@@ -6486,6 +6486,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 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: csso@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
...@@ -12938,6 +12943,14 @@ xmlchars@^2.2.0: ...@@ -12938,6 +12943,14 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== 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: xtend@^4.0.0, xtend@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 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