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

Merge pull request #2690 from blockscout/release/v2-0-0

Fixes for release `v2.0.0`
parents a76c14ca eb585d45
......@@ -15,7 +15,9 @@ on:
description: Image platforms (you can specify multiple platforms separated by comma)
required: false
type: string
default: linux/amd64
default: |
linux/amd64
linux/arm64/v8
workflow_call:
inputs:
tags:
......@@ -30,7 +32,9 @@ on:
description: Image platforms (you can specify multiple platforms separated by comma)
required: false
type: string
default: linux/amd64
default: |
linux/amd64
linux/arm64/v8
jobs:
run:
......@@ -40,9 +44,6 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta
id: meta
......@@ -55,13 +56,6 @@ jobs:
type=ref,event=tag
${{ inputs.tags }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
......@@ -73,6 +67,17 @@ jobs:
echo "ref_type: $REF_TYPE"
echo "ref_name: $REF_NAME"
- name: Setup repo
uses: blockscout/actions/.github/actions/setup-multiarch-buildx@no-metadata
id: setup
with:
docker-image: ghcr.io/blockscout/frontend
docker-username: ${{ github.actor }}
docker-password: ${{ secrets.GITHUB_TOKEN }}
docker-remote-multi-platform: true
docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }}
docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }}
- name: Build and push
uses: docker/build-push-action@v5
with:
......
......@@ -59,7 +59,7 @@ module.exports = {
{
userAgent: '*',
allow: '/',
disallow: ['/auth/*', '/login', '/chakra', '/sprite', '/account/*', '/api/*', '/node-api/*'],
disallow: ['/auth/*', '/login', '/chakra', '/sprite', '/account/*'],
},
],
},
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.78 7.28a.75.75 0 0 0-1.06-1.06l-6.97 6.97-3.47-3.47a.75.75 0 0 0-1.06 1.06l4 4a.75.75 0 0 0 1.06 0l7.5-7.5Z" fill="currentColor"/>
</svg>
import { useCallback, useRef, useEffect } from 'react';
import type { PreSubmitTransactionResponse, PreVerifyContractResponse } from '@blockscout/points-types';
import type { PreSubmitTransactionResponse } from '@blockscout/points-types';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
......@@ -74,17 +74,11 @@ export default function useRewardsActivity() {
[ makeRequest ],
);
const trackContract = useCallback(async(address: string) => {
return (
await makeRequest('rewards_user_activity_track_contract', {
address,
chain_id: config.chain.id ?? '',
})
) as PreVerifyContractResponse | undefined;
}, [ makeRequest ]);
const trackContractConfirm = useCallback((token: string) =>
makeRequest('rewards_user_activity_track_contract_confirm', { token }),
const trackContract = useCallback(async(address: string) =>
makeRequest('rewards_user_activity_track_contract', {
address,
chain_id: config.chain.id ?? '',
}),
[ makeRequest ],
);
......@@ -115,7 +109,6 @@ export default function useRewardsActivity() {
trackTransaction,
trackTransactionConfirm,
trackContract,
trackContractConfirm,
trackUsage,
};
}
......@@ -6,7 +6,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import useAccount from './useAccount';
export default function useAccountWithDomain(isEnabled: boolean) {
const { address } = useAccount();
const { address, isConnecting } = useAccount();
const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled);
......@@ -25,7 +25,7 @@ export default function useAccountWithDomain(isEnabled: boolean) {
return {
address: isEnabled ? address : undefined,
domain: domainQuery.data?.domain?.name,
isLoading: isQueryEnabled && domainQuery.isLoading,
isLoading: (isQueryEnabled && domainQuery.isLoading) || isConnecting,
};
}, [ address, domainQuery.data?.domain?.name, domainQuery.isLoading, isEnabled, isQueryEnabled ]);
}, [ address, domainQuery.data?.domain?.name, domainQuery.isLoading, isEnabled, isQueryEnabled, isConnecting ]);
}
......@@ -44,6 +44,7 @@
| "contracts/regular"
| "contracts/verified_many"
| "contracts/verified"
| "copy_check"
| "copy"
| "cross"
| "delete"
......
......@@ -43,7 +43,7 @@ export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
onError={ handleLoadError }
onLoad={ handleLoadSuccess }
{ ...rest }
display={ loading ? 'none' : rest.display || 'inline-block' }
display={ loading ? 'none' : rest.display || 'block' }
/>
</>
);
......
......@@ -234,10 +234,11 @@ export interface SelectProps extends SelectRootProps {
portalled?: boolean;
loading?: boolean;
errorText?: string;
contentProps?: SelectContentProps;
}
export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
const { collection, placeholder, portalled = true, loading, errorText, ...rest } = props;
const { collection, placeholder, portalled = true, loading, errorText, contentProps, ...rest } = props;
return (
<SelectRoot
ref={ ref }
......@@ -253,7 +254,7 @@ export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref)
errorText={ errorText }
/>
</SelectControl>
<SelectContent portalled={ portalled }>
<SelectContent portalled={ portalled } { ...contentProps }>
{ collection.items.map((item: SelectOption) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
......
......@@ -35,13 +35,30 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
lazyMount = true,
unmountOnExit = true,
triggerProps,
closeDelay = 100,
openDelay = 100,
interactive,
...rest
} = props;
const [ open, setOpen ] = React.useState<boolean>(defaultOpen);
const timeoutRef = React.useRef<number | null>(null);
const isMobile = useIsMobile();
const triggerRef = useClickAway<HTMLButtonElement>(() => setOpen(false));
const handleClickAway = React.useCallback((event: Event) => {
if (interactive) {
const closest = (event.target as HTMLElement)?.closest('.chakra-tooltip__positioner');
if (closest) {
return;
}
}
timeoutRef.current = window.setTimeout(() => {
setOpen(false);
}, closeDelay);
}, [ closeDelay, interactive ]);
const triggerRef = useClickAway<HTMLButtonElement>(handleClickAway);
const handleOpenChange = React.useCallback((details: { open: boolean }) => {
setOpen(details.open);
......@@ -49,8 +66,25 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
}, [ onOpenChange ]);
const handleTriggerClick = React.useCallback(() => {
setOpen((prev) => !prev);
}, [ ]);
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setOpen((prev) => !prev);
}, open ? closeDelay : openDelay);
}, [ open, openDelay, closeDelay ]);
const handleContentClick = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
}, []);
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
if (disabled || (disableOnMobile && isMobile)) return children;
......@@ -68,22 +102,23 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
return (
<ChakraTooltip.Root
openDelay={ 100 }
openDelay={ openDelay }
// FIXME: chakra closes tooltip too fast, so Playwright is not able to make a screenshot of its content
// so we need to increase the close delay in Playwright environment
closeDelay={ config.app.isPw ? 10_000 : 100 }
closeDelay={ config.app.isPw ? 10_000 : closeDelay }
open={ open }
onOpenChange={ handleOpenChange }
closeOnClick={ false }
closeOnPointerDown={ true }
closeOnPointerDown={ false }
variant={ variant }
lazyMount={ lazyMount }
unmountOnExit={ unmountOnExit }
interactive={ interactive }
{ ...rest }
positioning={ positioning }
>
<ChakraTooltip.Trigger
ref={ triggerRef }
ref={ open ? triggerRef : null }
asChild
onClick={ isMobile ? handleTriggerClick : undefined }
{ ...triggerProps }
......@@ -94,6 +129,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
<ChakraTooltip.Positioner>
<ChakraTooltip.Content
ref={ ref }
onClick={ interactive ? handleContentClick : undefined }
{ ...(selected ? { 'data-selected': true } : {}) }
{ ...contentProps }
>
......
......@@ -71,6 +71,7 @@ const AdaptiveTabs = (props: Props) => {
stickyEnabled={ stickyEnabled }
activeTab={ activeTab }
isLoading={ isLoading }
variant={ variant }
/>
{ tabs.map((tab) => {
const value = getTabValue(tab);
......
......@@ -9,6 +9,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import { useIsSticky } from '../..//hooks/useIsSticky';
import { Skeleton } from '../../chakra/skeleton';
import type { TabsProps } from '../../chakra/tabs';
import { TabsCounter, TabsList, TabsTrigger } from '../../chakra/tabs';
import AdaptiveTabsMenu from './AdaptiveTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
......@@ -32,6 +33,7 @@ export interface BaseProps {
interface Props extends BaseProps {
activeTab: string;
variant: TabsProps['variant'];
}
const HIDDEN_ITEM_STYLES: HTMLChakraProps<'button'> = {
......@@ -41,19 +43,17 @@ const HIDDEN_ITEM_STYLES: HTMLChakraProps<'button'> = {
visibility: 'hidden',
};
const getItemStyles = (index: number, tabsCut: number | undefined) => {
if (tabsCut === undefined) {
const getItemStyles = (index: number, tabsCut: number | undefined, isLoading: boolean | undefined) => {
if (tabsCut === undefined || isLoading) {
return HIDDEN_ITEM_STYLES as never;
}
return index < tabsCut ? {} : HIDDEN_ITEM_STYLES as never;
};
const getMenuStyles = (tabsLength: number, tabsCut: number | undefined) => {
if (tabsCut === undefined) {
return {
opacity: 0,
};
const getMenuStyles = (tabsLength: number, tabsCut: number | undefined, isLoading: boolean | undefined) => {
if (tabsCut === undefined || isLoading) {
return HIDDEN_ITEM_STYLES;
}
return tabsCut >= tabsLength ? HIDDEN_ITEM_STYLES : {};
......@@ -71,6 +71,7 @@ const AdaptiveTabsList = (props: Props) => {
leftSlotProps,
stickyEnabled,
isLoading,
variant,
} = props;
const scrollDirection = useScrollDirection();
......@@ -80,11 +81,13 @@ const AdaptiveTabsList = (props: Props) => {
return [ ...tabs, menuButton ];
}, [ tabs ]);
const { tabsCut, tabsRefs, listRef, rightSlotRef, leftSlotRef } = useAdaptiveTabs(tabsList, isMobile);
const { tabsCut, tabsRefs, listRef, rightSlotRef, leftSlotRef } = useAdaptiveTabs(tabsList, isLoading || isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled);
const activeTabIndex = tabsList.findIndex((tab) => getTabValue(tab) === activeTab) ?? 0;
useScrollToActiveTab({ activeTabIndex, listRef, tabsRefs, isMobile, isLoading });
const isReady = !isLoading && tabsCut !== undefined;
return (
<TabsList
ref={ listRef }
......@@ -92,10 +95,6 @@ const AdaptiveTabsList = (props: Props) => {
alignItems="center"
whiteSpace="nowrap"
bgColor={{ _light: 'white', _dark: 'black' }}
// initially our cut is 0 and we don't want to show the list
// but we want to keep all items in the tabs row so it won't collapse
// that's why we only change opacity but not the position itself
opacity={ tabsCut ? 1 : 0 }
marginBottom={ 6 }
mx={{ base: '-12px', lg: 'unset' }}
px={{ base: '12px', lg: 'unset' }}
......@@ -134,15 +133,11 @@ const AdaptiveTabsList = (props: Props) => {
</Box>
)
}
{ tabsList.slice(0, isLoading ? 5 : Infinity).map((tab, index) => {
{ tabsList.map((tab, index) => {
const value = getTabValue(tab);
const ref = tabsRefs[index];
if (tab.id === 'menu') {
if (isLoading) {
return null;
}
return (
<AdaptiveTabsMenu
key="menu"
......@@ -150,7 +145,7 @@ const AdaptiveTabsList = (props: Props) => {
tabs={ tabs }
tabsCut={ tabsCut ?? 0 }
isActive={ activeTabIndex > 0 && tabsCut !== undefined && tabsCut > 0 && activeTabIndex >= tabsCut }
{ ...getMenuStyles(tabs.length, tabsCut) }
{ ...getMenuStyles(tabs.length, tabsCut, isLoading) }
/>
);
}
......@@ -162,19 +157,30 @@ const AdaptiveTabsList = (props: Props) => {
ref={ ref }
scrollSnapAlign="start"
flexShrink={ 0 }
{ ...getItemStyles(index, tabsCut) }
{ ...getItemStyles(index, tabsCut, isLoading) }
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabsCounter count={ tab.count }/>
</TabsTrigger>
);
}) }
{ tabs.slice(0, isReady ? 0 : 5).map((tab, index) => {
const value = `${ getTabValue(tab) }-pre`;
return (
<TabsTrigger
key={ value }
value={ value }
flexShrink={ 0 }
bgColor={
activeTabIndex === index && (variant === 'solid' || variant === undefined) ?
{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } :
undefined
}
>
{ isLoading ? (
<Skeleton loading>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabsCounter count={ tab.count }/>
</Skeleton>
) : (
<>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabsCounter count={ tab.count }/>
</>
) }
<Skeleton loading>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabsCounter count={ tab.count }/>
</Skeleton>
</TabsTrigger>
);
}) }
......
import { Flex, chakra, Box } from '@chakra-ui/react';
import React from 'react';
import type { TabItemRegular } from '../AdaptiveTabs/types';
import { Skeleton } from '../../chakra/skeleton';
import type { TabsProps } from '../../chakra/tabs';
import useActiveTabFromQuery from './useActiveTabFromQuery';
const SkeletonTabText = ({ size, title }: { size: TabsProps['size']; title: TabItemRegular['title'] }) => (
<Skeleton
borderRadius="base"
borderWidth={ size === 'sm' ? '2px' : 0 }
fontWeight={ 600 }
mx={ size === 'sm' ? 3 : 4 }
flexShrink={ 0 }
loading
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
);
interface Props {
className?: string;
tabs: Array<TabItemRegular>;
size?: 'sm' | 'md';
}
const RoutedTabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
const activeTab = useActiveTabFromQuery(tabs);
if (tabs.length === 1) {
return null;
}
const tabIndex = activeTab ? tabs.findIndex((tab) => tab.id === activeTab.id) : 0;
return (
<Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ tabs.slice(0, tabIndex).map(({ title, id }) => (
<SkeletonTabText
key={ id.toString() }
title={ title }
size={ size }
/>
)) }
{ tabs.slice(tabIndex, tabIndex + 1).map(({ title, id }) => (
<Box
key={ id.toString() }
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
py={ size === 'sm' ? 1 : 2 }
borderRadius="base"
flexShrink={ 0 }
>
<SkeletonTabText
key={ id.toString() }
title={ title }
size={ size }
/>
</Box>
)) }
{ tabs.slice(tabIndex + 1).map(({ title, id }) => (
<SkeletonTabText
key={ id.toString() }
title={ title }
size={ size }
/>
)) }
</Flex>
);
};
export default chakra(RoutedTabsSkeleton);
export { default as RoutedTabs } from './RoutedTabs';
export { default as RoutedTabsSkeleton } from './RoutedTabsSkeleton';
export { default as useActiveTabFromQuery } from './useActiveTabFromQuery';
......@@ -11,9 +11,10 @@ export interface TruncatedTextTooltipProps {
children: React.ReactNode;
label: React.ReactNode;
placement?: Placement;
interactive?: boolean;
}
export const TruncatedTextTooltip = React.memo(({ children, label, placement }: TruncatedTextTooltipProps) => {
export const TruncatedTextTooltip = React.memo(({ children, label, placement, interactive }: TruncatedTextTooltipProps) => {
const childRef = React.useRef<HTMLElement>(null);
const [ isTruncated, setTruncated ] = React.useState(false);
const { open, onToggle, onOpen, onClose } = useDisclosure();
......@@ -83,6 +84,7 @@ export const TruncatedTextTooltip = React.memo(({ children, label, placement }:
contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
positioning={{ placement }}
open={ open }
interactive={ interactive }
>
{ modifiedChildren }
</Tooltip>
......
......@@ -3,17 +3,16 @@ export const zIndex = {
auto: { value: 'auto' },
base: { value: 0 },
docked: { value: 10 },
dropdown: { value: 1000 },
sticky: { value: 1100 },
sticky1: { value: 1101 },
sticky2: { value: 1102 },
popover: { value: 1150 },
banner: { value: 1200 },
overlay: { value: 1300 },
modal: { value: 1400 },
popover: { value: 1500 },
modal2: { value: 14001 },
tooltip: { value: 1550 }, // otherwise tooltips will not be visible in modals
tooltip2: { value: 1551 }, // for tooltips in tooltips
skipLink: { value: 1600 },
toast: { value: 1700 },
};
......
......@@ -16,7 +16,7 @@ export const recipe = defineRecipe({
bg: 'blue.600',
color: 'white',
_hover: {
bg: 'blue.400',
bg: 'link.primary.hover',
},
_loading: {
opacity: 1,
......@@ -27,7 +27,7 @@ export const recipe = defineRecipe({
},
},
_expanded: {
bg: 'blue.400',
bg: 'link.primary.hover',
},
},
outline: {
......@@ -38,8 +38,8 @@ export const recipe = defineRecipe({
borderColor: 'button.outline.fg',
_hover: {
bg: 'transparent',
color: 'blue.400',
borderColor: 'blue.400',
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
_loading: {
opacity: 1,
......@@ -58,8 +58,8 @@ export const recipe = defineRecipe({
borderColor: 'button.dropdown.border',
_hover: {
bg: 'transparent',
color: 'blue.400',
borderColor: 'blue.400',
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
_loading: {
opacity: 1,
......@@ -72,8 +72,8 @@ export const recipe = defineRecipe({
// When the dropdown is open, the button should be active
_expanded: {
bg: 'transparent',
color: 'blue.400',
borderColor: 'blue.400',
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
// We have a special state for this button variant that serves as a popover trigger.
// When any items (filters) are selected in the popover, the button should change its background and text color.
......@@ -84,9 +84,12 @@ export const recipe = defineRecipe({
borderColor: 'transparent',
_hover: {
bg: 'button.dropdown.bg.selected',
color: 'button.dropdown.fg.selected',
color: 'link.primary.hover',
borderColor: 'transparent',
},
_expanded: {
color: 'link.primary.hover',
},
},
},
header: {
......@@ -97,8 +100,8 @@ export const recipe = defineRecipe({
borderStyle: 'solid',
_hover: {
bg: 'transparent',
color: 'blue.400',
borderColor: 'blue.400',
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
_loading: {
opacity: 1,
......@@ -115,16 +118,22 @@ export const recipe = defineRecipe({
borderWidth: '0px',
_hover: {
bg: 'button.header.bg.selected',
color: 'button.header.fg.selected',
color: 'link.primary.hover',
},
_expanded: {
color: 'link.primary.hover',
},
_highlighted: {
bg: 'button.header.bg.highlighted',
color: 'button.header.fg.highlighted',
borderColor: 'transparent',
borderWidth: '0px',
_expanded: {
color: 'link.primary.hover',
},
_hover: {
bg: 'button.header.bg.highlighted',
color: 'button.header.fg.highlighted',
color: 'link.primary.hover',
},
},
},
......@@ -149,7 +158,10 @@ export const recipe = defineRecipe({
color: 'button.hero.fg.selected',
_hover: {
bg: 'button.hero.bg.selected',
color: 'button.hero.fg.selected',
color: 'link.primary.hover',
},
_expanded: {
color: 'link.primary.hover',
},
},
},
......@@ -230,7 +242,10 @@ export const recipe = defineRecipe({
color: 'button.icon_secondary.fg.selected',
_hover: {
bg: 'button.icon_secondary.bg.selected',
color: 'button.icon_secondary.fg.selected',
color: 'link.primary.hover',
},
_expanded: {
color: 'link.primary.hover',
},
},
_expanded: {
......
......@@ -207,7 +207,7 @@ export const recipe = defineSlotRecipe({
defaultVariants: {
size: 'md',
scrollBehavior: 'inside',
placement: 'center',
placement: { base: 'top', lg: 'center' },
motionPreset: 'scale',
},
});
......@@ -9,7 +9,7 @@ export const recipe = defineSlotRecipe({
boxShadow: 'popover',
color: 'initial',
maxHeight: 'var(--available-height)',
'--menu-z-index': 'zIndex.dropdown',
'--menu-z-index': 'zIndex.popover',
zIndex: 'calc(var(--menu-z-index) + var(--layer-index, 0))',
borderRadius: 'md',
overflow: 'hidden',
......@@ -25,7 +25,7 @@ export const recipe = defineSlotRecipe({
},
item: {
textDecoration: 'none',
color: 'initial',
color: 'text.primary',
userSelect: 'none',
borderRadius: 'none',
width: '100%',
......
......@@ -27,7 +27,7 @@ export type ScrollL2TxnBatch = {
confirmation_transaction: ScrollL2TxnBatchConfirmationTransaction;
start_block_number: number;
end_block_number: number;
transactions_count: number;
transactions_count: number | null;
data_availability: {
batch_data_container: 'in_blob4844' | 'in_calldata';
};
......
import { chakra } from '@chakra-ui/react';
import { pickBy } from 'es-toolkit';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
......@@ -26,11 +27,11 @@ const AddressAdvancedFilterLink = ({ isLoading, address, typeFilter, directionFi
return null;
}
const queryParams = {
const queryParams = pickBy({
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'),
};
}, (value) => value !== undefined);
return (
<Link
......
......@@ -85,36 +85,6 @@ test.describe('socket', () => {
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('without overload', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS } },
);
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</Box>,
{ hooksConfig },
{ withSocket: true },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with update', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
......@@ -136,13 +106,13 @@ test.describe('socket', () => {
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] });
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const secondRow = page.locator('tbody tr:nth-child(2)');
await secondRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
expect(itemsCountNew).toBe(2);
});
test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
......@@ -154,7 +124,7 @@ test.describe('socket', () => {
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
<AddressTxs/>
</Box>,
{ hooksConfig },
{ withSocket: true },
......@@ -205,10 +175,10 @@ test.describe('socket', () => {
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2 ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const secondRow = page.locator('tbody tr:nth-child(2)');
await secondRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
......@@ -229,7 +199,7 @@ test.describe('socket', () => {
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
<AddressTxs/>
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
......
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import type { TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
......@@ -21,45 +16,23 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import { sortTxsFromSocket } from 'ui/txs/sortTxs';
import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting';
import { SORT_OPTIONS } from 'ui/txs/useTxsSort';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
type Props = {
shouldRender?: boolean;
isQueryEnabled?: boolean;
// for tests only
overloadCount?: number;
};
const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMounted = useIsMounted();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const [ sort, setSort ] = React.useState<TransactionsSortingValue>(getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default');
const isMobile = useIsMobile();
......@@ -90,76 +63,6 @@ const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQue
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => {
setSocketAlert('');
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems: Array<Transaction> = [];
let newCount = 0;
payload.transactions.forEach(tx => {
const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
if (currIndex > -1) {
prevData.items[currIndex] = tx;
} else {
if (matchFilter(filterValue, tx, currentAddress)) {
if (newItems.length + prevData.items.length >= overloadCount) {
newCount++;
} else {
newItems.push(tx);
}
}
}
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
}
return {
...prevData,
items: [
...newItems,
...prevData.items,
].sort(sortTxsFromSocket(sort)),
};
});
}, [ currentAddress, filterValue, overloadCount, queryClient, sort ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
if (!isMounted || !shouldRender) {
return null;
}
......@@ -197,9 +100,7 @@ const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQue
query={ addressTxsQuery }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
socketType="address_txs"
top={ ACTION_BAR_HEIGHT_DESKTOP }
sorting={ sort }
setSort={ setSort }
......
......@@ -87,7 +87,6 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
<AddressEntity
mb={ 3 }
fontWeight={ 500 }
color="text"
address={{ hash }}
noLink
/>
......
......@@ -71,7 +71,7 @@ const FilterByColumn = ({ column, filters, columnName, handleFilterChange, searc
<TableColumnFilterWrapper
columnName="And/Or"
isLoading={ isLoading }
selected={ false }
selected
w="106px"
value={ filters.address_relation === 'and' ? 'AND' : 'OR' }
>
......
......@@ -51,10 +51,9 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const { handleSubmit, watch, formState, setError, reset, getFieldState, getValues, clearErrors } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>();
const activityToken = React.useRef<string | undefined>();
const apiFetch = useApiFetch();
const { trackContract, trackContractConfirm } = useRewardsActivity();
const { trackContract } = useRewardsActivity();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data);
......@@ -78,8 +77,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}
try {
const activityResponse = await trackContract(data.address);
activityToken.current = activityResponse?.token;
await trackContract(data.address);
await apiFetch('contract_verification_via', {
pathParams: { method: data.method[0], hash: data.address.toLowerCase() },
fetchParams: {
......@@ -132,13 +130,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
{ send_immediately: true },
);
if (activityToken.current) {
await trackContractConfirm(activityToken.current);
activityToken.current = undefined;
}
window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ setError, address, getValues, trackContractConfirm ]);
}, [ setError, address, getValues ]);
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
......
......@@ -14,7 +14,7 @@ import DepositsTableItem from './DepositsTableItem';
const DepositsTable = ({ items, top, isLoading }: Props) => {
return (
<TableRoot style={{ tableLayout: 'auto' }} minW="950px">
<TableRoot tableLayout="auto" minW="950px">
<TableHeaderSticky top={ top }>
<TableRow>
<TableColumnHeader>L1 block No</TableColumnHeader>
......@@ -26,7 +26,7 @@ const DepositsTable = ({ items, top, isLoading }: Props) => {
</TableHeaderSticky>
<TableBody>
{ items.map((item, index) => (
<DepositsTableItem key={ item.l2_transaction_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
<DepositsTableItem key={ `${ item.l2_transaction_hash }-${ index }` } item={ item } isLoading={ isLoading }/>
)) }
</TableBody>
</TableRoot>
......
......@@ -6,6 +6,7 @@ import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { Link } from 'toolkit/chakra/link';
const easterEggBadgeFeature = config.features.easterEggBadge;
......@@ -36,7 +37,7 @@ const CapybaraRunner = () => {
return (
<>
<Box as="h2" mt={ 12 } mb={ 2 } fontWeight={ 600 } fontSize="xl">Score 1000 to win a special prize!</Box>
<Heading level="2" mt={ 12 } mb={ 2 }>Score 1000 to win a special prize!</Heading>
<Box mb={ 4 }>{ isMobile ? 'Tap below to start' : 'Press space to start' }</Box>
<Script strategy="lazyOnload" src="/static/capibara/index.js"/>
<Box width={{ base: '100%', lg: '600px' }} height="300px" p="50px 0">
......
......@@ -6,10 +6,10 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import { Link } from 'toolkit/chakra/link';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useNewTxsSocket from 'ui/txs/socket/useTxsSocketTypeAll';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemMobile from './LatestTxsItemMobile';
......@@ -23,7 +23,7 @@ const LatestTransactions = () => {
},
});
const { num, socketAlert } = useNewTxsSocket();
const { num, alertText } = useNewTxsSocket({ type: 'txs_home', isLoading: isPlaceholderData });
if (isError) {
return <Text mt={ 4 }>No data. Please reload the page.</Text>;
......@@ -33,7 +33,7 @@ const LatestTransactions = () => {
const txsUrl = route({ pathname: '/txs' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert } isLoading={ isPlaceholderData }/>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ alertText } isLoading={ isPlaceholderData }/>
<Box mb={ 3 } display={{ base: 'block', lg: 'none' }}>
{ data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItemMobile
......
......@@ -126,10 +126,10 @@ const ChainIndicators = () => {
alignItems="stretch"
>
<Flex flexGrow={ 1 } flexDir="column">
<Flex alignItems="center">
<Skeleton loading={ isPlaceholderData } display="flex" alignItems="center" w="fit-content" columnGap={ 1 }>
<Text fontWeight={ 500 }>{ title }</Text>
{ hint && <Hint label={ hint } ml={ 1 }/> }
</Flex>
{ hint && <Hint label={ hint }/> }
</Skeleton>
<Flex mb={{ base: 0, lg: 2 }} mt={ 1 } alignItems="end">
{ valueTitle }
{ valueDiff }
......
......@@ -17,13 +17,13 @@ interface Props {
const InternalTxsTable = ({ data, currentAddress, isLoading }: Props) => {
return (
<AddressHighlightProvider>
<TableRoot>
<TableRoot minW="900px">
<TableHeaderSticky top={ 68 }>
<TableRow>
<TableColumnHeader width="15%">Parent txn hash</TableColumnHeader>
<TableColumnHeader width="180px">Parent txn hash</TableColumnHeader>
<TableColumnHeader width="15%">Type</TableColumnHeader>
<TableColumnHeader width="10%">Block</TableColumnHeader>
<TableColumnHeader width="40%">From/To</TableColumnHeader>
<TableColumnHeader width="15%">Block</TableColumnHeader>
<TableColumnHeader width="50%">From/To</TableColumnHeader>
<TableColumnHeader width="20%" isNumeric>
Value { currencyUnits.ether }
</TableColumnHeader>
......
......@@ -6,13 +6,13 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import config from 'configs/app';
import { Badge } from 'toolkit/chakra/badge';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import TruncatedValue from 'ui/shared/TruncatedValue';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress?: string; isLoading?: boolean };
......@@ -81,9 +81,13 @@ const InternalTxsTableItem = ({
/>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<Skeleton loading={ isLoading } display="inline-block" minW={ 6 }>
{ BigNumber(value).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }
</Skeleton>
<TruncatedValue
value={ BigNumber(value).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }
isLoading={ isLoading }
minW={ 6 }
maxW="100%"
verticalAlign="middle"
/>
</TableCell>
</TableRow>
);
......
......@@ -12,6 +12,7 @@ import config from 'configs/app';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import * as mixpanel from 'lib/mixpanel/index';
import { Link } from 'toolkit/chakra/link';
import type { PopoverContentProps } from 'toolkit/chakra/popover';
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import { apos } from 'toolkit/utils/htmlEntities';
......@@ -30,10 +31,20 @@ type Props = {
popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left';
buttonProps?: ButtonProps;
triggerWrapperProps?: BoxProps;
popoverContentProps?: PopoverContentProps;
};
const AppSecurityReport = ({
id, securityReport, showContractList, isLoading, onlyIcon, source, triggerWrapperProps, buttonProps, popoverPlacement = 'bottom-start',
id,
securityReport,
showContractList,
isLoading,
onlyIcon,
source,
triggerWrapperProps,
buttonProps,
popoverPlacement = 'bottom-start',
popoverContentProps,
}: Props) => {
const { open, onOpenChange } = useDisclosure();
......@@ -76,7 +87,7 @@ const AppSecurityReport = ({
wrapperProps={ triggerWrapperProps }
{ ...buttonProps }
/>
<PopoverContent w={{ base: 'calc(100vw - 48px)', lg: '328px' }} mx={{ base: 3, lg: 0 }}>
<PopoverContent w={{ base: 'calc(100vw - 48px)', lg: '328px' }} mx={{ base: 3, lg: 0 }} { ...popoverContentProps }>
<PopoverBody px="26px" py="20px" textStyle="sm">
<Text fontWeight="500" textStyle="xs" mb={ 2 } color="text.secondary">Smart contracts info</Text>
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
......
......@@ -58,7 +58,6 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
open={ Boolean(type) }
onOpenChange={ handleOpenChange }
size={{ lgDown: 'full', lg: 'md' }}
placement="center"
>
<DialogContent>
<DialogHeader display="flex" alignItems="center" mb={ 4 } onBackToClick={ onBack }>
......
......@@ -135,7 +135,6 @@ const MarketplaceAppModal = ({
open={ Boolean(data.id) }
onOpenChange={ handleOpenChange }
size={{ lgDown: 'full', lg: 'md' }}
placement="center"
>
<DialogContent>
<Box
......@@ -197,6 +196,7 @@ const MarketplaceAppModal = ({
fullView
canRate={ canRate }
source="App modal"
popoverContentProps={{ zIndex: 'modal' }}
/>
</Box>
) }
......@@ -265,6 +265,7 @@ const MarketplaceAppModal = ({
showContractList={ showContractList }
source="App modal"
popoverPlacement={ isMobile ? 'bottom-start' : 'left' }
popoverContentProps={{ zIndex: 'modal' }}
/>
</Flex>
</Flex>
......
......@@ -27,7 +27,6 @@ const MarketplaceDisclaimerModal = ({ isOpen, onClose, appId }: Props) => {
open={ isOpen }
onOpenChange={ onClose }
size={ isMobile ? 'full' : 'md' }
placement="center"
>
<DialogContent>
<DialogHeader>
......
......@@ -5,6 +5,7 @@ import type { AppRating } from 'types/client/marketplace';
import config from 'configs/app';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import type { PopoverContentProps } from 'toolkit/chakra/popover';
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import { Rating } from 'toolkit/chakra/rating';
import { Skeleton } from 'toolkit/chakra/skeleton';
......@@ -26,11 +27,13 @@ type Props = {
fullView?: boolean;
canRate: boolean | undefined;
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
popoverContentProps?: PopoverContentProps;
};
const MarketplaceRating = ({
appId, rating, userRating, rate,
isSending, isLoading, fullView, canRate, source,
popoverContentProps,
}: Props) => {
if (!isEnabled) {
......@@ -60,7 +63,7 @@ const MarketplaceRating = ({
canRate={ canRate }
/>
{ canRate ? (
<PopoverContent w="250px">
<PopoverContent w="250px" { ...popoverContentProps }>
<PopoverBody>
<Content
appId={ appId }
......
......@@ -137,7 +137,7 @@ const AdvancedFilter = () => {
const content = (
<AddressHighlightProvider>
<Box maxW="100%" overflowX="scroll" whiteSpace="nowrap">
<Box maxW="100%" display="grid" overflowX="scroll" whiteSpace="nowrap">
<TableRoot tableLayout="fixed" minWidth="950px" w="100%">
<TableHeaderSticky>
<TableRow>
......
......@@ -12,7 +12,6 @@ import { BLOCK } from 'stubs/block';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -86,7 +85,7 @@ const ArbitrumL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......@@ -116,15 +115,13 @@ const ArbitrumL2TxnBatch = () => {
backLink={ backLink }
isLoading={ batchQuery.isPlaceholderData }
/>
{ batchQuery.isPlaceholderData ?
<RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ batchQuery.isPlaceholderData }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
</>
);
};
......
......@@ -15,7 +15,6 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct
import getQueryParamString from 'lib/router/getQueryParamString';
import { Skeleton } from 'toolkit/chakra/skeleton';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import BlockCeloEpochTag from 'ui/block/BlockCeloEpochTag';
import BlockDetails from 'ui/block/BlockDetails';
import BlockEpochRewards from 'ui/block/BlockEpochRewards';
......@@ -74,7 +73,7 @@ const BlockPageContent = () => {
component: (
<>
{ blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
</>
),
},
......@@ -83,7 +82,7 @@ const BlockPageContent = () => {
id: 'blob_txs',
title: 'Blob txns',
component: (
<TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/>
<TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false }/>
),
} : null,
config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
......@@ -183,14 +182,13 @@ const BlockPageContent = () => {
secondRow={ titleSecondRow }
isLoading={ blockQuery.isPlaceholderData }
/>
{ blockQuery.isPlaceholderData ? <RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationParams) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ blockQuery.isPlaceholderData }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationParams) }/> : null }
stickyEnabled={ hasPagination }
/>
</>
);
};
......
......@@ -31,10 +31,7 @@ const KettleTxs = () => {
<>
<PageTitle title="Computor transactions" withTextAd/>
<AddressEntity address={{ hash }} mb={ 6 }/>
<TxsWithFrontendSorting
query={ query }
showSocketInfo={ false }
/>
<TxsWithFrontendSorting query={ query }/>
</>
);
};
......
......@@ -170,7 +170,7 @@ const Marketplace = () => {
<IconSvg name="dots"/>
</IconButton>
</MenuTrigger>
<MenuContent>
<MenuContent zIndex="banner">
{ links.map(({ label, href, icon }) => (
<MenuItem key={ label } value={ label } asChild>
<Link external href={ href } variant="menu" gap={ 0 }>
......@@ -205,6 +205,7 @@ const Marketplace = () => {
showShadow
display="flex"
flexDirection="column"
mt={ 0 }
mx={{ base: -3, lg: -12 }}
px={{ base: 3, lg: 12 }}
pt={{ base: 4, lg: 6 }}
......
......@@ -14,8 +14,6 @@ import { ENS_DOMAIN } from 'stubs/ENS';
import { Link } from 'toolkit/chakra/link';
import { Tooltip } from 'toolkit/chakra/tooltip';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import useActiveTabFromQuery from 'toolkit/components/RoutedTabs/useActiveTabFromQuery';
import NameDomainDetails from 'ui/nameDomain/NameDomainDetails';
import NameDomainHistory from 'ui/nameDomain/NameDomainHistory';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -40,8 +38,6 @@ const NameDomain = () => {
{ id: 'history', title: 'History', component: <NameDomainHistory domain={ infoQuery.data }/> },
];
const activeTab = useActiveTabFromQuery(tabs);
throwOnResourceLoadError(infoQuery);
const isLoading = infoQuery.isPlaceholderData;
......@@ -87,12 +83,7 @@ const NameDomain = () => {
<>
<TextAd mb={ 6 }/>
<PageTitle title="Name details" secondRow={ titleSecondRow }/>
{ infoQuery.isPlaceholderData ? (
<>
<RoutedTabsSkeleton tabs={ tabs } mt={ 6 }/>
{ activeTab?.component }
</>
) : <RoutedTabs tabs={ tabs }/> }
<RoutedTabs tabs={ tabs } isLoading={ infoQuery.isPlaceholderData }/>
</>
);
};
......
......@@ -12,7 +12,6 @@ import { BLOCK } from 'stubs/block';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -84,7 +83,7 @@ const OptimisticL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......@@ -114,15 +113,13 @@ const OptimisticL2TxnBatch = () => {
backLink={ backLink }
isLoading={ batchQuery.isPlaceholderData }
/>
{ batchQuery.isPlaceholderData ?
<RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ batchQuery.isPlaceholderData }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
</>
);
};
......
......@@ -14,7 +14,6 @@ import { SCROLL_L2_TXN_BATCH } from 'stubs/scrollL2';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -91,7 +90,7 @@ const ScrollL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......@@ -120,15 +119,13 @@ const ScrollL2TxnBatch = () => {
title={ `Txn batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ?
<RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ batchQuery.isPlaceholderData }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
</>
);
};
......
......@@ -42,7 +42,7 @@ const L2Deposits = () => {
<Box hideFrom="lg">
{ data.items.map(((item, index) => (
<DepositsListItem
key={ item.l2_transaction_hash + (isPlaceholderData ? index : '') }
key={ `${ item.l2_transaction_hash }-${ index }` }
isLoading={ isPlaceholderData }
item={ item }
/>
......
......@@ -14,7 +14,7 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import WithdrawalsListItem from 'ui/withdrawals/shibarium/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/shibarium/WithdrawalsTable';
const L2Withdrawals = () => {
const ShibariumWithdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'shibarium_withdrawals',
options: {
......@@ -42,7 +42,7 @@ const L2Withdrawals = () => {
<Box hideFrom="lg">
{ data.items.map(((item, index) => (
<WithdrawalsListItem
key={ item.l2_transaction_hash + (isPlaceholderData ? index : '') }
key={ `${ item.l2_transaction_hash }-${ index }` }
item={ item }
isLoading={ isPlaceholderData }
/>
......@@ -83,4 +83,4 @@ const L2Withdrawals = () => {
);
};
export default L2Withdrawals;
export default ShibariumWithdrawals;
......@@ -10,8 +10,6 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { publicClient } from 'lib/web3/client';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import useActiveTabFromQuery from 'toolkit/components/RoutedTabs/useActiveTabFromQuery';
import TextAd from 'ui/shared/ad/TextAd';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import EntityTags from 'ui/shared/EntityTags/EntityTags';
......@@ -78,8 +76,6 @@ const TransactionPageContent = () => {
].filter(Boolean);
})();
const activeTab = useActiveTabFromQuery(tabs);
const txTags: Array<TEntityTag> = data?.transaction_tag ?
[ { slug: data.transaction_tag, name: data.transaction_tag, tagType: 'private_tag' as const, ordinal: 1 } ] : [];
if (rollupFeature.isEnabled && rollupFeature.interopEnabled && data?.op_interop) {
......@@ -113,19 +109,6 @@ const TransactionPageContent = () => {
const titleSecondRow = <TxSubHeading hash={ hash } hasTag={ Boolean(data?.transaction_tag) } txQuery={ txQuery }/>;
const content = (() => {
if (isPlaceholderData && !showDegradedView) {
return (
<>
<RoutedTabsSkeleton tabs={ tabs } mt={ 6 }/>
{ activeTab?.component }
</>
);
}
return <RoutedTabs tabs={ tabs }/>;
})();
if (isError && !showDegradedView) {
if (isCustomAppError(error)) {
throwOnResourceLoadError({ resource: 'tx', error, isError: true });
......@@ -141,7 +124,7 @@ const TransactionPageContent = () => {
contentAfter={ tags }
secondRow={ titleSecondRow }
/>
{ content }
<RoutedTabs tabs={ tabs } isLoading={ isPlaceholderData }/>
</>
);
};
......
......@@ -9,7 +9,6 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
......@@ -91,8 +90,6 @@ const Transactions = () => {
},
});
const { num, socketAlert } = useNewTxsSocket();
const isAuth = useIsAuth();
const tabs: Array<TabItemRegular> = [
......@@ -102,9 +99,7 @@ const Transactions = () => {
component:
<TxsWithFrontendSorting
query={ txsValidatedQuery }
showSocketInfo={ txsValidatedQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
socketType="txs_validated"
top={ TABS_HEIGHT }
/> },
{
......@@ -114,9 +109,7 @@ const Transactions = () => {
<TxsWithFrontendSorting
query={ txsPendingQuery }
showBlockInfo={ false }
showSocketInfo={ txsPendingQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
socketType="txs_pending"
top={ TABS_HEIGHT }
/>
),
......@@ -127,9 +120,6 @@ const Transactions = () => {
component: (
<TxsWithFrontendSorting
query={ txsWithBlobsQuery }
showSocketInfo={ txsWithBlobsQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
top={ TABS_HEIGHT }
/>
),
......
......@@ -13,8 +13,6 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OP } from 'stubs/userOps';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import useActiveTabFromQuery from 'toolkit/components/RoutedTabs/useActiveTabFromQuery';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import TxLogs from 'ui/tx/TxLogs';
......@@ -82,8 +80,6 @@ const UserOp = () => {
{ id: 'raw', title: 'Raw', component: <UserOpRaw rawData={ userOpQuery.data?.raw } isLoading={ userOpQuery.isPlaceholderData }/> },
]), [ userOpQuery, txQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]);
const activeTab = useActiveTabFromQuery(tabs);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/ops');
......@@ -110,13 +106,7 @@ const UserOp = () => {
backLink={ backLink }
secondRow={ titleSecondRow }
/>
{ userOpQuery.isPlaceholderData ? (
<>
<RoutedTabsSkeleton tabs={ tabs } mt={ 6 }/>
{ activeTab?.component }
</>
) :
<RoutedTabs tabs={ tabs }/> }
<RoutedTabs tabs={ tabs } isLoading={ userOpQuery.isPlaceholderData }/>
</>
);
};
......
......@@ -12,7 +12,6 @@ import { TX_ZKEVM_L2 } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { ZKEVM_L2_TXN_BATCH } from 'stubs/zkEvmL2';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
......@@ -48,7 +47,7 @@ const ZkEvmL2TxnBatch = () => {
const tabs: Array<TabItemRegular> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery }/> },
].filter(Boolean)), [ batchQuery, batchTxsQuery ]);
const backLink = React.useMemo(() => {
......@@ -71,11 +70,10 @@ const ZkEvmL2TxnBatch = () => {
title={ `Txn batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ? <RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ batchQuery.isPlaceholderData }
/>
</>
);
};
......
......@@ -13,7 +13,6 @@ import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { ZKSYNC_L2_TXN_BATCH } from 'stubs/zkSyncL2';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -68,7 +67,7 @@ const ZkSyncL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, hasPagination ]);
......@@ -92,15 +91,13 @@ const ZkSyncL2TxnBatch = () => {
title={ `Txn batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ?
<RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(batchTxsQuery.pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ batchQuery.isPlaceholderData }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(batchTxsQuery.pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
</>
);
};
......
......@@ -14,6 +14,7 @@ import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useIsMobile from 'lib/hooks/useIsMobile';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { FormFieldEmail } from 'toolkit/components/forms/fields/FormFieldEmail';
import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText';
import { FormFieldUrl } from 'toolkit/components/forms/fields/FormFieldUrl';
......@@ -96,8 +97,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
rowGap={ 3 }
templateColumns={{ base: '1fr', lg: '1fr 1fr minmax(0, 200px)', xl: '1fr 1fr minmax(0, 250px)' }}
>
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
Company info
<GridItem colSpan={{ base: 1, lg: 3 }}>
<Heading level="2">
Company info
</Heading>
</GridItem>
<FormFieldText<FormFields> name="requesterName" required placeholder="Your name"/>
<FormFieldEmail<FormFields> name="requesterEmail" required/>
......@@ -107,9 +110,11 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
<FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website"/>
{ !isMobile && <div/> }
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link.primary"/>
<GridItem colSpan={{ base: 1, lg: 3 }} mt={{ base: 3, lg: 5 }}>
<Heading level="2" display="flex" alignItems="center" columnGap={ 1 }>
Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review"/>
</Heading>
</GridItem>
<PublicTagsSubmitFieldAddresses/>
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
......
import { Box, Flex, Grid, GridItem } from '@chakra-ui/react';
import { Flex, Grid, GridItem } from '@chakra-ui/react';
import { pickBy } from 'es-toolkit';
import React from 'react';
......@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { Link } from 'toolkit/chakra/link';
import { makePrettyLink } from 'toolkit/utils/url';
......@@ -43,7 +44,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
</Alert>
) }
<Box as="h2" textStyle="h4">Company info</Box>
<Heading level="2">Company info</Heading>
<Grid rowGap={ 3 } columnGap={ 6 } gridTemplateColumns="170px 1fr" mt={ 6 }>
<GridItem>Your name</GridItem>
<GridItem>{ groupedData.requesterName }</GridItem>
......@@ -65,7 +66,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
) }
</Grid>
<Box as="h2" textStyle="h4" mt={ 8 } mb={ 5 }>Public tags/labels</Box>
<Heading level="2" mt={ 8 } mb={ 5 }>Public tags/labels</Heading>
{ hasErrors ? <PublicTagsSubmitResultWithErrors data={ groupedData }/> : <PublicTagsSubmitResultSuccess data={ groupedData }/> }
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 6 } mt={ 8 } rowGap={ 3 }>
......
......@@ -290,7 +290,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
<Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }>
<Skeleton loading={ isLoading } overflow="hidden" display="flex" alignItems="center">
<Text whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } noTooltip/>
</Text>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Skeleton>
......
......@@ -23,9 +23,8 @@ const ButtonItem = ({ className, label, onClick, icon, isDisabled }: Props) => {
disabled={ isDisabled }
variant="icon_secondary"
boxSize={ 8 }
_icon={{ boxSize: 6 }}
>
{ typeof icon === 'string' ? <IconSvg name={ icon }/> : icon }
{ typeof icon === 'string' ? <IconSvg name={ icon } boxSize={ 6 }/> : icon }
</IconButton>
</Tooltip>
);
......
......@@ -29,6 +29,7 @@ const ActionBar = ({ children, className, showShadow }: Props) => {
className={ className }
backgroundColor={{ _light: 'white', _dark: 'black' }}
pt={ 6 }
mt={ -6 }
pb={{ base: 6, lg: 3 }}
mx={{ base: -3, lg: 0 }}
px={{ base: 3, lg: 0 }}
......
......@@ -10,10 +10,14 @@ export interface Props extends Omit<IconButtonProps, 'type' | 'loading'> {
text: string;
type?: 'link' | 'text' | 'share';
isLoading?: boolean;
// Chakra v3 doesn't support tooltip inside tooltip - https://github.com/chakra-ui/chakra-ui/issues/9939#issuecomment-2817168121
// so we disable the copy tooltip manually when the button is inside a tooltip
noTooltip?: boolean;
tooltipInteractive?: boolean;
}
const CopyToClipboard = (props: Props) => {
const { text, type = 'text', isLoading, onClick, boxSize = 5, ...rest } = props;
const { text, type = 'text', isLoading, onClick, boxSize = 5, noTooltip, tooltipInteractive, ...rest } = props;
const { hasCopied, copy, disclosure } = useClipboard(text);
......@@ -23,6 +27,37 @@ const CopyToClipboard = (props: Props) => {
onClick?.(event);
}, [ onClick, copy ]);
const iconName = (() => {
switch (type) {
case 'link':
return hasCopied ? 'check' : 'link';
case 'share':
return hasCopied ? 'check' : 'share';
default:
return hasCopied ? 'copy_check' : 'copy';
}
})();
const button = (
<IconButton
aria-label="copy"
boxSize={ boxSize }
onClick={ handleClick }
ml={ 2 }
borderRadius="sm"
loadingSkeleton={ isLoading }
variant="icon_secondary"
size="2xs"
{ ...rest }
>
<IconSvg name={ iconName }/>
</IconButton>
);
if (noTooltip) {
return button;
}
const tooltipContent = (() => {
if (hasCopied) {
return 'Copied';
......@@ -35,17 +70,6 @@ const CopyToClipboard = (props: Props) => {
return 'Copy to clipboard';
})();
const iconName = (() => {
switch (type) {
case 'link':
return 'link';
case 'share':
return 'share';
default:
return 'copy';
}
})();
return (
<Tooltip
content={ tooltipContent }
......@@ -53,20 +77,9 @@ const CopyToClipboard = (props: Props) => {
open={ disclosure.open }
onOpenChange={ disclosure.onOpenChange }
closeOnPointerDown={ false }
interactive={ tooltipInteractive }
>
<IconButton
aria-label="copy"
boxSize={ boxSize }
onClick={ handleClick }
ml={ 2 }
borderRadius="sm"
loadingSkeleton={ isLoading }
variant="icon_secondary"
size="2xs"
{ ...rest }
>
<IconSvg name={ iconName }/>
</IconButton>
{ button }
</Tooltip>
);
};
......
......@@ -6,20 +6,27 @@ import { Tooltip } from 'toolkit/chakra/tooltip';
interface Props {
hash: string;
isTooltipDisabled?: boolean;
noTooltip?: boolean;
tooltipInteractive?: boolean;
type?: 'long' | 'short';
as?: React.ElementType;
}
const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props) => {
const HashStringShorten = ({ hash, noTooltip, as = 'span', type, tooltipInteractive }: Props) => {
const charNumber = type === 'long' ? 16 : 8;
if (hash.length <= charNumber) {
return <chakra.span as={ as }>{ hash }</chakra.span>;
}
const content = <chakra.span as={ as }>{ shortenString(hash, charNumber) }</chakra.span>;
if (noTooltip) {
return content;
}
return (
<Tooltip content={ hash } disabled={ isTooltipDisabled }>
<chakra.span as={ as }>{ shortenString(hash, charNumber) }</chakra.span>
<Tooltip content={ hash } interactive={ tooltipInteractive }>
{ content }
</Tooltip>
);
};
......
......@@ -23,12 +23,13 @@ const HEAD_MIN_LENGTH = 4;
interface Props {
hash: string;
fontWeight?: string | number;
isTooltipDisabled?: boolean;
noTooltip?: boolean;
tooltipInteractive?: boolean;
tailLength?: number;
as?: React.ElementType;
}
const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, tailLength = TAIL_LENGTH, as = 'span' }: Props) => {
const HashStringShortenDynamic = ({ hash, fontWeight = '400', noTooltip, tailLength = TAIL_LENGTH, as = 'span', tooltipInteractive }: Props) => {
const elementRef = useRef<HTMLSpanElement>(null);
const [ displayedString, setDisplayedString ] = React.useState(hash);
......@@ -93,12 +94,12 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
const content = <chakra.span ref={ elementRef } as={ as }>{ displayedString }</chakra.span>;
const isTruncated = hash.length !== displayedString.length;
if (isTruncated) {
if (isTruncated && !noTooltip) {
return (
<Tooltip
content={ hash }
disabled={ isTooltipDisabled }
contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
interactive={ tooltipInteractive }
>
{ content }
</Tooltip>
......
......@@ -10,11 +10,12 @@ interface Props {
isLoading?: boolean;
value: string;
tooltipPlacement?: Placement;
tooltipInteractive?: boolean;
}
const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props) => {
const TruncatedValue = ({ className, isLoading, value, tooltipPlacement, tooltipInteractive }: Props) => {
return (
<TruncatedTextTooltip label={ value } placement={ tooltipPlacement }>
<TruncatedTextTooltip label={ value } placement={ tooltipPlacement } interactive={ tooltipInteractive }>
<Skeleton
className={ className }
loading={ isLoading }
......
......@@ -40,7 +40,7 @@ const init = () => {
'--w3m-font-family': `${ BODY_TYPEFACE }, sans-serif`,
'--w3m-accent': colors.blue[600].value,
'--w3m-border-radius-master': '2px',
'--w3m-z-index': zIndex?.popover?.value,
'--w3m-z-index': zIndex?.modal2?.value,
},
featuredWalletIds: [],
allowUnsupportedChain: true,
......
......@@ -36,7 +36,9 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Pick<EntityProps, 'address' | 'isSafeAddress'> & EntityBase.IconBaseProps;
type IconProps = Pick<EntityProps, 'address' | 'isSafeAddress'> & EntityBase.IconBaseProps & {
tooltipInteractive?: boolean;
};
const Icon = (props: IconProps) => {
if (props.noIcon) {
......@@ -70,7 +72,7 @@ const Icon = (props: IconProps) => {
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract');
return (
<Tooltip content={ label.slice(0, 1).toUpperCase() + label.slice(1) }>
<Tooltip content={ label.slice(0, 1).toUpperCase() + label.slice(1) } interactive={ props.tooltipInteractive }>
<span>
<EntityBase.Icon
{ ...props }
......@@ -90,7 +92,7 @@ const Icon = (props: IconProps) => {
})();
return (
<Tooltip content={ label } disabled={ !label }>
<Tooltip content={ label } disabled={ !label } interactive={ props.tooltipInteractive }>
<Flex marginRight={ styles.marginRight } position="relative">
<AddressIdenticon
size={ props.variant === 'heading' ? 30 : 20 }
......@@ -128,7 +130,12 @@ const Content = chakra((props: ContentProps) => {
);
return (
<Tooltip content={ label } contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}>
<Tooltip
content={ label }
contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
triggerProps={{ minW: 0 }}
interactive={ props.tooltipInteractive }
>
<Skeleton loading={ props.isLoading } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" { ...styles }>
{ nameText }
</Skeleton>
......@@ -174,7 +181,10 @@ const AddressEntity = (props: EntityProps) => {
const settingsContext = useSettingsContext();
const altHash = !props.noAltHash && settingsContext?.addressFormat === 'bech32' ? toBech32Address(props.address.hash) : undefined;
const content = <Content { ...partsProps.content } altHash={ altHash }/>;
// inside highlight context all tooltips should be interactive
// because non-interactive ones will not pass 'onMouseLeave' event to the parent component
// see issue - https://github.com/chakra-ui/chakra-ui/issues/9939#issuecomment-2810567024
const content = <Content { ...partsProps.content } altHash={ altHash } tooltipInteractive={ Boolean(highlightContext) }/>;
return (
<Container
......@@ -187,9 +197,9 @@ const AddressEntity = (props: EntityProps) => {
position="relative"
zIndex={ 0 }
>
<Icon { ...partsProps.icon }/>
<Icon { ...partsProps.icon } tooltipInteractive={ Boolean(highlightContext) }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
<Copy { ...partsProps.copy } altHash={ altHash }/>
<Copy { ...partsProps.copy } altHash={ altHash } tooltipInteractive={ Boolean(highlightContext) }/>
</Container>
);
};
......
......@@ -25,7 +25,14 @@ const AddressEntityContentProxy = (props: ContentProps) => {
Proxy contract
{ props.address.name ? ` (${ props.address.name })` : '' }
</Box>
<AddressEntity address={{ hash: props.address.hash, filecoin: props.address.filecoin }} noLink noIcon noHighlight justifyContent="center"/>
<AddressEntity
address={{ hash: props.address.hash, filecoin: props.address.filecoin }}
noLink
noIcon
noHighlight
noTooltip
justifyContent="center"
/>
<Box fontWeight={ 600 } mt={ 2 }>
Implementation{ implementations.length > 1 ? 's' : '' }
{ implementationName ? ` (${ implementationName })` : '' }
......@@ -38,10 +45,10 @@ const AddressEntityContentProxy = (props: ContentProps) => {
noLink
noIcon
noHighlight
noTooltip
minW={ `calc((100% - ${ colNum - 1 } * 12px) / ${ colNum })` }
flex={ 1 }
justifyContent={ colNum === 1 ? 'center' : undefined }
isTooltipDisabled
/>
)) }
</Flex>
......@@ -49,13 +56,13 @@ const AddressEntityContentProxy = (props: ContentProps) => {
);
return (
<Tooltip content={ content } interactive contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '410px' } }}>
<Tooltip content={ content } interactive contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '410px' } }} triggerProps={{ minW: 0 }}>
<Box display="inline-flex" w="100%">
<EntityBase.Content
{ ...props }
truncation={ nameTag || implementationName || props.address.name ? 'tail' : props.truncation }
text={ nameTag || implementationName || props.address.name || props.altHash || props.address.hash }
isTooltipDisabled
noTooltip
/>
</Box>
</Tooltip>
......
......@@ -23,7 +23,7 @@ export interface EntityBaseProps {
icon?: EntityIconProps;
isExternal?: boolean;
isLoading?: boolean;
isTooltipDisabled?: boolean;
noTooltip?: boolean;
noCopy?: boolean;
noIcon?: boolean;
noLink?: boolean;
......@@ -112,12 +112,23 @@ const Icon = ({ isLoading, noIcon, variant, name, color, borderRadius, marginRig
);
};
export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength' | 'isTooltipDisabled' | 'variant'> {
export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength' | 'noTooltip' | 'variant'> {
asProp?: React.ElementType;
text: string;
tooltipInteractive?: boolean;
}
const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dynamic', tailLength, isTooltipDisabled, variant }: ContentBaseProps) => {
const Content = chakra(({
className,
isLoading,
asProp,
text,
truncation = 'dynamic',
tailLength,
variant,
noTooltip,
tooltipInteractive,
}: ContentBaseProps) => {
const styles = getContentProps(variant);
if (truncation === 'tail') {
......@@ -126,6 +137,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
className={ className }
isLoading={ isLoading }
value={ text }
tooltipInteractive={ tooltipInteractive }
{ ...styles }
/>
);
......@@ -139,7 +151,8 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
hash={ text }
as={ asProp }
type="long"
isTooltipDisabled={ isTooltipDisabled }
noTooltip={ noTooltip }
tooltipInteractive={ tooltipInteractive }
/>
);
case 'constant':
......@@ -147,7 +160,8 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
<HashStringShorten
hash={ text }
as={ asProp }
isTooltipDisabled={ isTooltipDisabled }
noTooltip={ noTooltip }
tooltipInteractive={ tooltipInteractive }
/>
);
case 'dynamic':
......@@ -156,7 +170,8 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
hash={ text }
as={ asProp }
tailLength={ tailLength }
isTooltipDisabled={ isTooltipDisabled }
noTooltip={ noTooltip }
tooltipInteractive={ tooltipInteractive }
/>
);
case 'none':
......@@ -178,7 +193,10 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
);
});
export type CopyBaseProps = Pick<CopyToClipboardProps, 'isLoading' | 'text'> & Pick<EntityBaseProps, 'noCopy'>;
export type CopyBaseProps =
Pick<CopyToClipboardProps, 'isLoading' | 'text' | 'tooltipInteractive'> &
Pick<EntityBaseProps, 'noCopy' | 'noTooltip'>
;
const Copy = ({ noCopy, ...props }: CopyBaseProps) => {
if (noCopy) {
......
......@@ -22,6 +22,12 @@ const FilterButton = ({ isLoading, appliedFiltersNum, ...rest }: Props, ref: Rea
size={ 5 }
bg={{ _light: 'blue.700', _dark: 'gray.50' }}
color={{ _light: 'white', _dark: 'black' }}
_groupHover={{
bg: 'link.primary.hover',
}}
_groupExpanded={{
bg: 'link.primary.hover',
}}
>
{ appliedFiltersNum }
</Circle>
......
......@@ -14,7 +14,7 @@ interface Props {
const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isLoading }: Props) => {
return (
<PopoverRoot>
<PopoverTrigger>
<PopoverTrigger className="group">
<FilterButton
appliedFiltersNum={ appliedFiltersNum }
isLoading={ isLoading }
......
......@@ -21,7 +21,7 @@ const TableColumnFilterWrapper = ({ columnName, className, children, isLoading,
<Button
display="inline-flex"
aria-label={ `filter by ${ columnName }` }
variant="dropdown"
variant="icon_secondary"
borderWidth="0"
h="20px"
minW="auto"
......
......@@ -224,7 +224,7 @@ const TxInterpretation = ({ summary, isLoading, addressDataMap, className, isNov
<Tooltip content="Human readable transaction provided by Noves.fi">
<Badge ml={ 2 } verticalAlign="unset" transform="translateY(-2px)">
by
<Image src={ novesLogoUrl } alt="Noves logo" h="12px" ml={ 1.5 }/>
<Image src={ novesLogoUrl } alt="Noves logo" h="12px" ml={ 1.5 } display="inline"/>
</Badge>
</Tooltip>
) }
......
......@@ -3,7 +3,6 @@ import React from 'react';
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'toolkit/chakra/tabs';
import AdaptiveTabs from 'toolkit/components/AdaptiveTabs/AdaptiveTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import { Section, Container, SectionHeader, SamplesStack, Sample, SectionSubHeader } from './parts';
......@@ -74,13 +73,6 @@ const TabsShowcase = () => {
/>
</Sample>
</SamplesStack>
<SectionSubHeader>Tabs skeleton</SectionSubHeader>
<SamplesStack>
<Sample>
<RoutedTabsSkeleton tabs={ tabs }/>
</Sample>
</SamplesStack>
</Section>
</Container>
);
......
......@@ -15,7 +15,7 @@ const TooltipShowcase = () => {
<Tooltip content="Tooltip content">
<span>Default</span>
</Tooltip>
<Tooltip content="Tooltip content">
<Tooltip content="Tooltip content" interactive>
<Utilization value={ 0.5 }/>
</Tooltip>
</Sample>
......
......@@ -35,6 +35,7 @@ test('base view', async({ render, page }) => {
await expect(page).toHaveScreenshot();
await page.getByRole('button', { name: 'Network menu' }).click();
await page.mouse.move(0, 0);
await expect(page).toHaveScreenshot();
});
......@@ -48,6 +49,7 @@ test.describe('dark mode', () => {
await expect(page).toHaveScreenshot();
await page.getByRole('button', { name: 'Network menu' }).click();
await page.mouse.move(0, 0);
await expect(page).toHaveScreenshot();
});
});
......
......@@ -58,6 +58,7 @@ const NetworkMenuContentMobile = ({ items, tabs }: Props) => {
collection={ selectCollection }
placeholder="Select network type"
mb={ 3 }
contentProps={{ zIndex: 'modal' }}
/>
) }
<VStack as="ul" gap={ 2 } alignItems="stretch">
......
......@@ -159,6 +159,7 @@ const SearchBar = ({ isHomepage }: Props) => {
w={ `${ menuWidth.current }px` }
ref={ menuRef }
overflow="hidden"
zIndex="modal"
>
<PopoverBody
p={ 0 }
......
......@@ -139,7 +139,7 @@ const SearchBarInput = (
position={{ base: isHomepage ? 'static' : 'absolute', lg: 'relative' }}
top={{ base: isHomepage ? 0 : 55, lg: 0 }}
left="0"
zIndex={{ base: isHomepage ? 'auto' : '0', lg: isSuggestOpen ? 'popover' : 'auto' }}
zIndex={{ base: isHomepage ? 'auto' : '0', lg: isSuggestOpen ? 'modal' : 'auto' }}
paddingX={{ base: isHomepage ? 0 : 3, lg: 0 }}
paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }}
paddingBottom={{ base: isHomepage ? 0 : 2, lg: 0 }}
......
......@@ -80,7 +80,7 @@ const SearchBarSuggest = ({ onClick, onClear }: Props) => {
>
{ kw.startsWith('0x') ? (
<Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ kw } isTooltipDisabled/>
<HashStringShortenDynamic hash={ kw } noTooltip/>
</Box>
) :
<Text overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis">{ kw }</Text>
......
......@@ -55,7 +55,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }:
const tagEl = data.type === 'metadata_tag' ? (
<SearchResultEntityTag metadata={ data.metadata } searchTerm={ searchTerm } ml={{ base: 0, lg: 'auto' }}/>
) : null;
const addressEl = <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>;
const addressEl = <HashStringShortenDynamic hash={ hash } noTooltip/>;
if (isMobile) {
return (
......
......@@ -12,7 +12,7 @@ const SearchBarSuggestBlob = ({ data }: ItemsProps<SearchResultBlob>) => {
<Flex alignItems="center" minW={ 0 }>
<BlobEntity.Icon/>
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<HashStringShortenDynamic hash={ data.blob_hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ data.blob_hash } noTooltip/>
</chakra.mark>
</Flex>
);
......
......@@ -33,7 +33,7 @@ const SearchBarSuggestBlock = ({ data, isMobile, searchTerm }: ItemsProps<Search
as={ shouldHighlightHash ? 'mark' : 'span' }
display="block"
>
<HashStringShortenDynamic hash={ data.block_hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ data.block_hash } noTooltip/>
</Text>
) : null;
const date = !isFutureBlock ? dayjs(data.timestamp).format('llll') : undefined;
......
......@@ -32,7 +32,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: I
whiteSpace="nowrap"
color="text.secondary"
>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } noTooltip/>
</Text>
);
......
......@@ -30,7 +30,7 @@ const SearchBarSuggestLabel = ({ data, isMobile, searchTerm, addressFormat }: It
whiteSpace="nowrap"
color="text.secondary"
>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } noTooltip/>
</Text>
);
......
......@@ -30,7 +30,7 @@ const SearchBarSuggestToken = ({ data, isMobile, searchTerm, addressFormat }: It
const address = (
<Text color="text.secondary" whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } noTooltip/>
</Text>
);
......
......@@ -12,7 +12,7 @@ const SearchBarSuggestTx = ({ data, isMobile }: ItemsProps<SearchResultTx>) => {
const icon = <TxEntity.Icon/>;
const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<HashStringShortenDynamic hash={ data.transaction_hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ data.transaction_hash } noTooltip/>
</chakra.mark>
);
const date = dayjs(data.timestamp).format('llll');
......
......@@ -12,7 +12,7 @@ const SearchBarSuggestUserOp = ({ data, isMobile }: ItemsProps<SearchResultUserO
const icon = <UserOpEntity.Icon/>;
const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
<HashStringShortenDynamic hash={ data.user_operation_hash } isTooltipDisabled/>
<HashStringShortenDynamic hash={ data.user_operation_hash } noTooltip/>
</chakra.mark>
);
const date = dayjs(data.timestamp).format('llll');
......
......@@ -42,8 +42,11 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending, ..
e.preventDefault();
}, []);
const isButtonLoading = isPending || !isFetched || web3AccountWithDomain.isLoading;
const dataExists = !isButtonLoading && (Boolean(data) || Boolean(web3AccountWithDomain.address));
const content = (() => {
if (web3AccountWithDomain.address) {
if (web3AccountWithDomain.address && !isButtonLoading) {
return (
<HStack gap={ 2 }>
<UserIdenticon address={ web3AccountWithDomain.address } isAutoConnectDisabled={ isAutoConnectDisabled }/>
......@@ -54,7 +57,7 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending, ..
);
}
if (!data) {
if (!data || isButtonLoading) {
return 'Log in';
}
......@@ -66,9 +69,6 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending, ..
);
})();
const isButtonLoading = isPending || !isFetched;
const dataExists = !isButtonLoading && (Boolean(data) || Boolean(web3AccountWithDomain.address));
return (
<Tooltip
content={ <span>Sign in to My Account to add tags,<br/>create watchlists, access API keys and more</span> }
......
......@@ -51,11 +51,11 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
<AddressEntity
address={{ hash: web3AccountWithDomain.address, ens_domain_name: web3AccountWithDomain.domain }}
isLoading={ web3AccountWithDomain.isLoading }
isTooltipDisabled
truncation="dynamic"
fontSize="sm"
fontWeight={ 500 }
noAltHash
noTooltip
onClick={ handleAddressClick }
/>
{ web3Wallet.isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : (
......
......@@ -35,7 +35,7 @@ const UserWalletMenuContent = ({ isAutoConnectDisabled, address, domain, isRecon
<Flex alignItems="center" columnGap={ 2 } justifyContent="space-between">
<AddressEntity
address={{ hash: address, ens_domain_name: domain }}
isTooltipDisabled
noTooltip
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
......
......@@ -14,6 +14,7 @@ import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import type { EntityProps as AddressEntityProps } from 'ui/shared/entities/address/AddressEntity';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = {
token: TokenInfo;
......@@ -104,14 +105,18 @@ const TokensTableItem = ({
</Flex>
</TableCell>
<TableCell isNumeric>
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 } display="inline-block">
{ exchangeRate && `$${ Number(exchangeRate).toLocaleString(undefined, { minimumSignificantDigits: 4 }) }` }
</Skeleton>
<TruncatedValue
value={ exchangeRate ? `$${ Number(exchangeRate).toLocaleString(undefined, { minimumSignificantDigits: 4 }) }` : '' }
isLoading={ isLoading }
maxW="100%"
/>
</TableCell>
<TableCell isNumeric maxWidth="300px" width="300px">
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 } display="inline-block">
{ marketCap && `$${ BigNumber(marketCap).toFormat() }` }
</Skeleton>
<TruncatedValue
value={ marketCap ? `$${ BigNumber(marketCap).toFormat() }` : '' }
isLoading={ isLoading }
maxW="100%"
/>
</TableCell>
<TableCell isNumeric>
<Skeleton
......
......@@ -115,17 +115,21 @@ const ScrollL2TxnBatchDetails = ({ query }: Props) => {
}
</DetailedInfo.ItemValue>
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Link loading={ isPlaceholderData } href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transactions_count.toLocaleString() } transaction{ data.transactions_count === 1 ? '' : 's' }
</Link>
</DetailedInfo.ItemValue>
{ typeof data.transactions_count === 'number' ? (
<>
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Link loading={ isPlaceholderData } href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transactions_count.toLocaleString() } transaction{ data.transactions_count === 1 ? '' : 's' }
</Link>
</DetailedInfo.ItemValue>
</>
) : null }
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
......
......@@ -104,17 +104,21 @@ const ScrollL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
</Link>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
fontWeight={ 600 }
minW="40px"
>
{ item.transactions_count.toLocaleString() }
</Link>
</ListItemMobileGrid.Value>
{ typeof item.transactions_count === 'number' ? (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
fontWeight={ 600 }
minW="40px"
>
{ item.transactions_count.toLocaleString() }
</Link>
</ListItemMobileGrid.Value>
</>
) : null }
</ListItemMobileGrid.Container>
);
......
......@@ -87,12 +87,14 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
</Link>
</TableCell>
<TableCell verticalAlign="middle" isNumeric>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
>
{ item.transactions_count.toLocaleString() }
</Link>
{ typeof item.transactions_count === 'number' ? (
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
>
{ item.transactions_count.toLocaleString() }
</Link>
) : 'N/A' }
</TableCell>
</TableRow>
);
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
......@@ -26,9 +27,7 @@ type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -46,9 +45,7 @@ const TxsContent = ({
filter,
filterValue,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -72,9 +69,7 @@ const TxsContent = ({
<Box hideFrom="lg">
<TxsList
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
isLoading={ isPlaceholderData }
enableTimeIncrement={ enableTimeIncrement }
currentAddress={ currentAddress }
......@@ -87,9 +82,7 @@ const TxsContent = ({
sort={ sort }
onSortToggle={ onSortToggle }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
top={ top || (query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0) }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { Transaction } from 'types/api/transaction';
import useInitialList from 'lib/hooks/useInitialList';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsSocketNotice from './socket/TxsSocketNotice';
import TxsListItem from './TxsListItem';
interface Props {
showBlockInfo: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
enableTimeIncrement?: boolean;
currentAddress?: string;
isLoading: boolean;
......@@ -30,14 +29,7 @@ const TxsList = (props: Props) => {
return (
<Box>
{ props.showSocketInfo && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ props.socketInfoNum }
alert={ props.socketInfoAlert }
isLoading={ props.isLoading }
/>
) }
{ props.socketType && <TxsSocketNotice type={ props.socketType } place="list" isLoading={ props.isLoading }/> }
{ props.items.slice(0, renderedItemsNum).map((tx, index) => (
<TxsListItem
key={ tx.hash + (props.isLoading ? index : '') }
......
......@@ -15,7 +15,6 @@ test('base view +@dark-mode', async({ render }) => {
onSortToggle={ () => {} }
top={ 0 }
showBlockInfo
showSocketInfo={ false }
/>,
);
......@@ -36,7 +35,6 @@ test.describe('screen xl', () => {
onSortToggle={ () => {} }
top={ 0 }
showBlockInfo
showSocketInfo={ false }
/>,
);
......
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import config from 'configs/app';
......@@ -8,8 +9,8 @@ import useInitialList from 'lib/hooks/useInitialList';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import { currencyUnits } from 'lib/units';
import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsSocketNotice from './socket/TxsSocketNotice';
import TxsTableItem from './TxsTableItem';
type Props = {
......@@ -18,9 +19,7 @@ type Props = {
onSortToggle: (field: TransactionsSortingField) => void;
top: number;
showBlockInfo: boolean;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
enableTimeIncrement?: boolean;
isLoading?: boolean;
......@@ -32,9 +31,7 @@ const TxsTable = ({
onSortToggle,
top,
showBlockInfo,
showSocketInfo,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
isLoading,
......@@ -96,14 +93,7 @@ const TxsTable = ({
</TableRow>
</TableHeaderSticky>
<TableBody>
{ showSocketInfo && (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
isLoading={ isLoading }
/>
) }
{ socketType && <TxsSocketNotice type={ socketType } place="table" isLoading={ isLoading }/> }
{ txs.slice(0, renderedItemsNum).map((item, index) => (
<TxsTableItem
key={ item.hash + (isLoading ? index : '') }
......
......@@ -101,7 +101,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
</TableCell>
{ !config.UI.views.tx.hiddenFields?.value && (
<TableCell isNumeric>
<CurrencyValue value={ tx.value } accuracy={ 8 } isLoading={ isLoading }/>
<CurrencyValue value={ tx.value } accuracy={ 8 } isLoading={ isLoading } wordBreak="break-word"/>
</TableCell>
) }
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
......@@ -112,6 +112,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
isLoading={ isLoading }
withCurrency={ Boolean(tx.celo || tx.stability_fee) }
justifyContent="end"
wordBreak="break-word"
/>
</TableCell>
) }
......
......@@ -10,7 +10,7 @@ type Props = {
const TxsWatchlist = ({ query }: Props) => {
useRedirectForInvalidAuthToken();
return <TxsWithFrontendSorting query={ query } showSocketInfo={ false } top={ 88 }/>;
return <TxsWithFrontendSorting query={ query } top={ 88 }/>;
};
export default TxsWatchlist;
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { TransactionsSortingValue } from 'types/api/transaction';
......@@ -12,9 +13,7 @@ type Props = {
query: QueryWithPagesResult<'address_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -29,9 +28,7 @@ const TxsWithAPISorting = ({
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -49,9 +46,7 @@ const TxsWithAPISorting = ({
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
......
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
......@@ -11,9 +12,7 @@ type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -26,9 +25,7 @@ const TxsWithFrontendSorting = ({
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -40,9 +37,7 @@ const TxsWithFrontendSorting = ({
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
......
import React from 'react';
import type { TxsSocketNoticePlace, TxsSocketType } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import TxsSocketNoticeTypeAddress from './TxsSocketNoticeTypeAddress';
import TxsSocketNoticeTypeAll from './TxsSocketNoticeTypeAll';
interface Props {
type: TxsSocketType;
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNotice = ({ type, place, isLoading }: Props) => {
const isMobile = useIsMobile();
if ((isMobile && place === 'table') || (!isMobile && place === 'list')) {
return null;
}
switch (type) {
case 'txs_home':
case 'txs_validated':
case 'txs_pending': {
return <TxsSocketNoticeTypeAll type={ type } place={ place } isLoading={ isLoading }/>;
}
case 'address_txs': {
return <TxsSocketNoticeTypeAddress place={ place } isLoading={ isLoading }/>;
}
default:
return null;
}
};
export default React.memo(TxsSocketNotice);
import React from 'react';
import type { TxsSocketNoticePlace } from './types';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useTxsSocketTypeAddress from './useTxsSocketTypeAddress';
interface Props {
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNoticeTypeAddress = ({ place, isLoading }: Props) => {
const { num, alertText } = useTxsSocketTypeAddress({ isLoading });
if (num === undefined) {
return null;
}
if (place === 'table') {
return (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ alertText }
num={ num }
isLoading={ isLoading }
/>
);
}
if (place === 'list') {
return (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ num }
alert={ alertText }
isLoading={ isLoading }
/>
);
}
};
export default React.memo(TxsSocketNoticeTypeAddress);
import React from 'react';
import type { TxsSocketNoticePlace, TxsSocketType } from './types';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useNewTxsSocketTypeAll from './useTxsSocketTypeAll';
interface Props {
type: TxsSocketType;
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNoticeTypeAll = ({ type, place, isLoading }: Props) => {
const { num, alertText } = useNewTxsSocketTypeAll({ type, isLoading });
if (num === undefined) {
return null;
}
if (place === 'table') {
return (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ alertText }
num={ num }
isLoading={ isLoading }
/>
);
}
if (place === 'list') {
return (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ num }
alert={ alertText }
isLoading={ isLoading }
/>
);
}
};
export default React.memo(TxsSocketNoticeTypeAll);
export type TxsSocketType = 'txs_validated' | 'txs_pending' | 'txs_home' | 'address_txs';
export type TxsSocketNoticePlace = 'list' | 'table';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import type { Transaction, TransactionsSortingValue } from 'types/api/transaction';
import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import { sortTxsFromSocket } from '../sortTxs';
import { SORT_OPTIONS } from '../useTxsSort';
const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
const OVERLOAD_COUNT = config.app.isPw ? 2 : 75;
interface Params {
isLoading?: boolean;
}
export default function useTxsSocketTypeAddress({ isLoading }: Params) {
const [ alertText, setAlertText ] = React.useState('');
const [ num, setNum ] = React.useState(0);
const router = useRouter();
const queryClient = useQueryClient();
const currentAddress = getQueryParamString(router.query.hash);
const filterValue = getQueryParamString(router.query.filter);
const page = getQueryParamString(router.query.page);
const sort = getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default';
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => {
setAlertText('');
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: filterValue ? { filter: filterValue } : undefined }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems: Array<Transaction> = [];
let newCount = 0;
payload.transactions.forEach(tx => {
const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
if (currIndex > -1) {
prevData.items[currIndex] = tx;
} else {
const isMatch = matchFilter(filterValue as AddressFromToFilter, tx, currentAddress);
if (isMatch) {
if (newItems.length + prevData.items.length >= OVERLOAD_COUNT) {
newCount++;
} else {
newItems.push(tx);
}
}
}
});
if (newCount > 0) {
setNum(prev => prev + newCount);
}
return {
...prevData,
items: [
...newItems,
...prevData.items,
].sort(sortTxsFromSocket(sort)),
};
});
}, [ currentAddress, filterValue, queryClient, sort ]);
const handleSocketClose = React.useCallback(() => {
setAlertText('Connection is lost. Please refresh the page to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setAlertText('An error has occurred while fetching new transactions. Please refresh the page.');
}, []);
const isDisabled = Boolean((page && page !== '1') || isLoading);
const channel = useSocketChannel({
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
if (isDisabled) {
return { };
}
return { num, alertText };
}
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react';
import type { TxsSocketType } from './types';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
function getSocketParams(router: NextRouter) {
if (
router.pathname === '/txs' &&
(router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
if (router.pathname === '/') {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
function getSocketParams(type: TxsSocketType, page: string) {
if (
router.pathname === '/txs' &&
router.query.tab === 'pending' &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
switch (type) {
case 'txs_home': {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
case 'txs_validated': {
return !page || page === '1' ? { topic: 'transactions:new_transaction' as const, event: 'transaction' as const } : {};
}
case 'txs_pending': {
return !page || page === '1' ? { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const } : {};
}
default:
return {};
}
return {};
}
function assertIsNewTxResponse(response: unknown): response is { transaction: number } {
......@@ -40,12 +32,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_
return typeof response === 'object' && response !== null && 'pending_transaction' in response;
}
export default function useNewTxsSocket() {
interface Params {
type: TxsSocketType;
isLoading?: boolean;
}
export default function useNewTxsSocketTypeAll({ type, isLoading }: Params) {
const router = useRouter();
const page = getQueryParamString(router.query.page);
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ alertText, setAlertText ] = React.useState('');
const { topic, event } = getSocketParams(router);
const { topic, event } = getSocketParams(type, page);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) {
......@@ -57,18 +56,18 @@ export default function useNewTxsSocket() {
}, [ setNum ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload the page.');
setAlertText('Connection is lost. Please reload the page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload the page.');
setAlertText('An error has occurred while fetching new transactions. Please reload the page.');
}, []);
const channel = useSocketChannel({
topic,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: !topic,
isDisabled: !topic || Boolean(isLoading),
});
useSocketMessage({
......@@ -78,8 +77,8 @@ export default function useNewTxsSocket() {
});
if (!topic && !event) {
return {};
return { };
}
return { num, socketAlert };
return { num, alertText };
}
......@@ -25,7 +25,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) =>
}, [ sort, setSorting ]);
return (
<TableRoot>
<TableRoot minW="950px">
<TableHeaderSticky top={ ACTION_BAR_HEIGHT_DESKTOP }>
<TableRow>
<TableColumnHeader width="50%">Contract</TableColumnHeader>
......
......@@ -64,7 +64,7 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
value={ balance }
isLoading={ isLoading }
my={ 1 }
w="100%"
maxW="100%"
/>
</TableCell>
<TableCell isNumeric>
......
......@@ -45,11 +45,11 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEmail }
const showNotificationToast = useCallback((isOn: boolean) => {
toaster.success({
title: 'Success',
description: !isOn ? 'Email notification is ON' : 'Email notification is OFF',
description: isOn ? 'Email notification is ON' : 'Email notification is OFF',
});
}, [ ]);
const { mutate } = useMutation({
const { mutate } = useMutation<WatchlistAddress>({
mutationFn: () => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
......@@ -57,16 +57,16 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEmail }
return apiFetch('watchlist', {
pathParams: { id: String(item.id) },
fetchParams: { method: 'PUT', body },
});
}) as Promise<WatchlistAddress>;
},
onError: () => {
showErrorToast();
setNotificationEnabled(prevState => !prevState);
setSwitchDisabled(false);
},
onSuccess: () => {
onSuccess: (data) => {
setSwitchDisabled(false);
showNotificationToast(notificationEnabled);
showNotificationToast(data.notification_methods.email);
},
});
......
......@@ -44,11 +44,11 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEm
const showNotificationToast = useCallback((isOn: boolean) => {
toaster.success({
title: 'Success',
description: !isOn ? 'Email notification is ON' : 'Email notification is OFF',
description: isOn ? 'Email notification is ON' : 'Email notification is OFF',
});
}, [ ]);
const { mutate } = useMutation({
const { mutate } = useMutation<WatchlistAddress>({
mutationFn: () => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
......@@ -56,16 +56,16 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEm
return apiFetch('watchlist', {
pathParams: { id: String(item.id) },
fetchParams: { method: 'PUT', body },
});
}) as Promise<WatchlistAddress>;
},
onError: () => {
showErrorToast();
setNotificationEnabled(prevState => !prevState);
setSwitchDisabled(false);
},
onSuccess: () => {
onSuccess: (data) => {
setSwitchDisabled(false);
showNotificationToast(!notificationEnabled);
showNotificationToast(data.notification_methods.email);
},
});
......
......@@ -14,7 +14,7 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return (
<TableRoot style={{ tableLayout: 'auto' }} minW="950px">
<TableRoot tableLayout="auto" minW="950px">
<TableHeaderSticky top={ top }>
<TableRow>
<TableColumnHeader>L2 block No</TableColumnHeader>
......@@ -26,7 +26,7 @@ const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
</TableHeaderSticky>
<TableBody>
{ items.map((item, index) => (
<WithdrawalsTableItem key={ item.l2_transaction_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
<WithdrawalsTableItem key={ `${ item.l2_transaction_hash }-${ index }` } item={ item } isLoading={ isLoading }/>
)) }
</TableBody>
</TableRoot>
......
......@@ -1536,10 +1536,10 @@
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66"
integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ==
"@blockscout/points-types@1.3.0-alpha.1":
version "1.3.0-alpha.1"
resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.3.0-alpha.1.tgz#d1f255de6ccfa09b8a938ffe17f6aedd559273a3"
integrity sha512-yZcxvPpS1JT79dZrzSeP4r3BM5cqSnsVnclCIpJMUO3qBRWEytVfDGXcqNacwqp3342Im8RB/YPLKAuJGc+CrA==
"@blockscout/points-types@1.3.0-alpha.2":
version "1.3.0-alpha.2"
resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.3.0-alpha.2.tgz#0308dcb4eef0dadf96f43b144835470e9f78f64f"
integrity sha512-tXCA51q3y08caCm7UhGyj+xsP0pd6yBhjElDHxEzM5SRop3culMiacaBXd0OPBszHjA0YdYgXFymuJhofB22ig==
"@blockscout/stats-types@2.5.0-alpha":
version "2.5.0-alpha"
......
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