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 }}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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