Commit 9c4c16ac authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #2772 from blockscout/release/v2-1-0

Fixes for release v2.1.0
parents 02393619 7d9f94e4
......@@ -7,12 +7,12 @@ const title = 'Ton Application Chain (TAC)';
const tonExplorerUrl = getEnvValue('NEXT_PUBLIC_TAC_TON_EXPLORER_URL');
const config: Feature<{ explorerUrl: string }> = (() => {
const config: Feature<{ tonExplorerUrl: string }> = (() => {
if (apis.tac && tonExplorerUrl) {
return Object.freeze({
title,
isEnabled: true,
explorerUrl: tonExplorerUrl,
tonExplorerUrl,
});
}
......
......@@ -3,7 +3,7 @@
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=tac_turin"
NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=https://tac-operation-lifecycle.k8s-dev.blockscout.com
NEXT_PUBLIC_TAC_TON_EXPLORER_URL=https://testnet.tonscan.org
NEXT_PUBLIC_TAC_TON_EXPLORER_URL=https://testnet.tonviewer.com
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
......
This diff is collapsed.
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.05.194a10 10 0 1 1 3.903 19.614A10 10 0 0 1 8.049.194ZM10 1.539a8.462 8.462 0 1 0 0 16.925 8.462 8.462 0 0 0 0-16.925Zm0 1.538a.77.77 0 0 1 .77.77v5.838l4.007 4a.771.771 0 0 1 .169.841.772.772 0 0 1-1.01.417.77.77 0 0 1-.251-.165l-4.23-4.232A.77.77 0 0 1 9.23 10V3.847a.77.77 0 0 1 .77-.77Z" fill="currentColor"/>
<path d="M19.697 9.64a9.704 9.704 0 0 0-1.434-4.73l-.194-.303a9.705 9.705 0 0 0-4.026-3.43l-.33-.143A9.703 9.703 0 0 0 8.461.419l-.354.064a9.705 9.705 0 0 0-4.71 2.405l-.259.25a9.705 9.705 0 0 0-2.58 4.618l-.075.35a9.705 9.705 0 0 0 .552 5.607l.143.33a9.704 9.704 0 0 0 3.43 4.025l.303.194a9.706 9.706 0 0 0 5.09 1.442l.48-.012a9.704 9.704 0 0 0 6.38-2.83l.332-.349A9.706 9.706 0 0 0 19.703 10l-.006-.359Zm-1.518-.047a8.189 8.189 0 0 0-2.11-5.09l-.28-.293a8.188 8.188 0 0 0-5.383-2.389L10 1.811a8.19 8.19 0 0 0-4.294 1.217l-.256.163A8.19 8.19 0 0 0 2.556 6.59l-.121.278a8.19 8.19 0 0 0-.466 4.73l.065.297A8.188 8.188 0 0 0 4.21 15.79l.218.21a8.19 8.19 0 0 0 3.974 2.03l.298.054a8.19 8.19 0 0 0 4.433-.52l.278-.12a8.19 8.19 0 0 0 3.397-2.895l.164-.255a8.19 8.19 0 0 0 1.216-4.295l-.01-.406ZM10.61 3.937a.61.61 0 0 0-.102-.339l-.077-.092a.61.61 0 0 0-.311-.168L10 3.327a.61.61 0 0 0-.338.102l-.093.077a.61.61 0 0 0-.179.43V10c0 .08.015.16.045.235l.056.105a.618.618 0 0 0 .075.092l4.17 4.17c.056.055.123.1.198.13l.115.035c.039.008.079.01.118.01h.002l.119-.01a.615.615 0 0 0 .115-.035l.107-.056a.601.601 0 0 0 .092-.075l.077-.093a.609.609 0 0 0 .057-.106l.035-.114.011-.12a.67.67 0 0 0-.011-.12l-.035-.115a.61.61 0 0 0-.134-.198L10.61 9.75V3.937Zm7.869 6.376a8.486 8.486 0 0 1-1.255 4.136l-.17.264a8.486 8.486 0 0 1-3.52 2.999l-.287.126a8.486 8.486 0 0 1-4.594.538l-.309-.055a8.484 8.484 0 0 1-4.117-2.104L4 15.999a8.485 8.485 0 0 1-2.254-4.037l-.068-.307a8.486 8.486 0 0 1 .484-4.902l.126-.288a8.486 8.486 0 0 1 2.999-3.52l.263-.17A8.486 8.486 0 0 1 10 1.516l.42.01A8.485 8.485 0 0 1 16 4l.289.305A8.484 8.484 0 0 1 18.485 10l-.006.314Zm1.509.182a10.002 10.002 0 0 1-2.575 6.216l-.342.36a10.001 10.001 0 0 1-6.575 2.916L10 20a10 10 0 0 1-5.244-1.486l-.311-.2A10.002 10.002 0 0 1 .91 14.166l-.148-.34a10.001 10.001 0 0 1-.57-5.777l.079-.362A10.002 10.002 0 0 1 2.928 2.93l.268-.257A10.001 10.001 0 0 1 8.049.192l.365-.065a10 10 0 0 1 5.413.634l.34.148a10.003 10.003 0 0 1 4.148 3.535l.2.312A10.002 10.002 0 0 1 20 9.999l-.012.496Zm-9.081-.869 3.904 3.899a.907.907 0 0 1 .198.294l.03.085c.026.085.04.174.04.263l-.005.09a.903.903 0 0 1-.035.174l-.03.085a.908.908 0 0 1-.138.229l-.06.066a.896.896 0 0 1-.216.157l-.08.037a.907.907 0 0 1-.61.03l-.083-.03a.904.904 0 0 1-.295-.194l-4.17-4.169a.914.914 0 0 1-.157-.214l-.038-.081a.91.91 0 0 1-.068-.349V3.937c0-.24.096-.471.265-.64l.067-.06A.906.906 0 0 1 10 3.03l.09.005a.914.914 0 0 1 .551.261l.06.066a.906.906 0 0 1 .206.575v5.69Z" fill="currentColor"/>
<path d="M8.164.859a9.3 9.3 0 0 1 8.3 2.457l.047.046c.136.134.268.272.395.415l.03.033c.207.234.403.478.587.733l.039.055.15.214c.087.131.17.266.25.401.15.252.29.51.415.773.012.024.022.049.033.073.084.18.161.362.233.546l.03.08a9.3 9.3 0 0 1 .607 3.297l-.012.46a9.301 9.301 0 0 1-2.395 5.781l-.317.334a9.3 9.3 0 0 1-6.114 2.712l-.461.012A9.3 9.3 0 0 1 5.105 17.9l-.29-.187a9.303 9.303 0 0 1-.819-.615l-.055-.049a9.33 9.33 0 0 1-.438-.397l-.098-.098a8.814 8.814 0 0 1-.351-.371l-.078-.088a9.32 9.32 0 0 1-.16-.188l-.062-.075a9.397 9.397 0 0 1-.294-.383A9.297 9.297 0 0 1 8.164.859Zm1.817 1.509a7.617 7.617 0 0 0-3.994 1.13l-.237.153A7.616 7.616 0 0 0 3.06 6.81l-.113.259a7.615 7.615 0 0 0-.433 4.398l.06.277a7.613 7.613 0 0 0 2.024 3.622l.203.196a7.615 7.615 0 0 0 3.695 1.888l.278.05a7.614 7.614 0 0 0 4.12-.484l.26-.112a7.615 7.615 0 0 0 3.159-2.692l.152-.237a7.616 7.616 0 0 0 1.13-3.993l-.008-.378a7.614 7.614 0 0 0-1.962-4.732l-.26-.274a7.614 7.614 0 0 0-5.005-2.22l-.378-.01Zm.083 1.138c.192.02.374.104.512.242l.056.062c.123.15.191.339.191.534v5.29l3.631 3.626c.079.078.142.17.185.274l.027.078c.024.08.037.162.037.245l-.005.084a.84.84 0 0 1-.032.162l-.027.078a.852.852 0 0 1-.129.213l-.057.062a.837.837 0 0 1-.199.146l-.075.034a.841.841 0 0 1-.566.029l-.079-.029a.842.842 0 0 1-.274-.18l-3.877-3.877a.853.853 0 0 1-.146-.2l-.036-.075a.846.846 0 0 1-.063-.323V4.344c0-.223.09-.438.247-.596l.061-.055a.844.844 0 0 1 .535-.192l.083.005Z" fill="currentColor"/>
</svg>
import getPageType from './getPageType';
import getPageType, { PAGE_TYPE_DICT } from './getPageType';
import logEvent from './logEvent';
import reset from './reset';
import useInit from './useInit';
......@@ -13,4 +13,5 @@ export {
getPageType,
userProfile,
reset,
PAGE_TYPE_DICT,
};
......@@ -22,11 +22,11 @@ export function getTacOperationStatus(type: tac.OperationType) {
}
export function getTacOperationStage(data: tac.OperationDetails, txHash: string) {
const currentStep = data.status_history.find((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase()));
if (!currentStep) {
return null;
const currentStep = data.status_history.filter((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase()));
if (currentStep.length === 0) {
return;
}
return STATUS_LABELS[currentStep.type];
return currentStep.map((step) => STATUS_LABELS[step.type]);
}
export const STATUS_SEQUENCE: Array<tac.OperationStage_StageType> = [
......
......@@ -107,6 +107,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
],
tac: [
[ 'NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ],
[ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonscan.org' ],
[ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonviewer.com' ],
],
};
......@@ -4,7 +4,7 @@ import { ADDRESS_HASH } from './addressParams';
export const TAC_OPERATION: tac.OperationBriefDetails = {
operation_id: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
type: tac.OperationType.PENDING,
type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z',
sender: {
address: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
......@@ -14,7 +14,7 @@ export const TAC_OPERATION: tac.OperationBriefDetails = {
export const TAC_OPERATION_DETAILS: tac.OperationDetails = {
operation_id: '0x6e7cdeea3f39e7664597a44ddb33ce47ba061cbee2992e2c7b0e3f9294ff8b30',
type: tac.OperationType.PENDING,
type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z',
sender: {
address: ADDRESS_HASH,
......
......@@ -32,6 +32,10 @@ type Props = {
const DateInput = ({ value, onChange, placeholder, max }: { value: string; onChange: (value: string) => void; placeholder: string; max: string }) => {
const [ tempValue, setTempValue ] = React.useState(value ? dayjs(value).format('YYYY-MM-DD') : '');
React.useEffect(() => {
setTempValue(value ? dayjs(value).format('YYYY-MM-DD') : '');
}, [ value ]);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setTempValue(event.target.value);
onChange(event.target.value);
......@@ -96,7 +100,7 @@ const AgeFilter = ({ value = defaultValue, handleFilterChange, onClose }: Props)
<TableColumnFilter
title="Set last duration"
isFilled={ Boolean(currentValue.from || currentValue.to || currentValue.age) }
isTouched={ currentValue.age ? value.age !== currentValue.age : Boolean(currentValue.from && currentValue.to && !isEqual(currentValue, value)) }
isTouched={ currentValue.age ? value.age !== currentValue.age : !isEqual(currentValue, value) }
onFilter={ onFilter }
onReset={ onReset }
hasReset
......
......@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Link } from 'toolkit/chakra/link';
import { BackToButton } from 'toolkit/components/buttons/BackToButton';
import { makePrettyLink } from 'toolkit/utils/url';
......@@ -46,6 +47,10 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []);
const hideContractList = React.useCallback(() => setContractListType(undefined), []);
const handleBackToClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Back to', Source: mixpanel.PAGE_TYPE_DICT['/apps/[id]'] });
}, []);
return (
<>
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
......@@ -54,6 +59,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
href={ goBackUrl }
hint="Back to dApps list"
loading={ isLoading }
onClick={ handleBackToClick }
/>
<Link
external
......
......@@ -7,6 +7,7 @@ import { sortStatusHistory } from 'lib/operations/tac';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp';
import AddressEntityTacTon from 'ui/shared/entities/address/AddressEntityTacTon';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import TacOperationLifecycleAccordion from './TacOperationLifecycleAccordion';
......@@ -43,6 +44,16 @@ const TacOperationDetails = ({ isLoading, data }: Props) => {
</>
) }
<DetailedInfo.ItemLabel
hint="The status of the operation"
isLoading={ isLoading }
>
Status
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<TacOperationStatus status={ data.type } isLoading={ isLoading }/>
</DetailedInfo.ItemValue>
{ data.timestamp && (
<>
<DetailedInfo.ItemLabel
......@@ -66,7 +77,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => {
Lifecycle
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading }/>
<TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading } type={ data.type }/>
</DetailedInfo.ItemValue>
</>
) }
......
import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import { AccordionItem, AccordionRoot } from 'toolkit/chakra/accordion';
......@@ -10,13 +10,16 @@ import TacOperationLifecycleAccordionItemTrigger from './TacOperationLifecycleAc
interface Props {
data: tac.OperationDetails['status_history'];
isLoading?: boolean;
type: tac.OperationType;
}
const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => {
const TacOperationLifecycleAccordion = ({ data, isLoading, type }: Props) => {
const isPending = type === tac.OperationType.PENDING && !isLoading;
return (
<AccordionRoot maxW="800px" display="flex" flexDirection="column" rowGap={ 6 } lazyMount>
{ data.map((item, index) => {
const isLast = index === data.length - 1;
const isLast = index === data.length - 1 && !isPending;
return (
<AccordionItem key={ index } value={ item.type } borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger
......@@ -33,6 +36,17 @@ const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => {
</AccordionItem>
);
}) }
{ isPending && (
<AccordionItem value="pending" borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger
status="pending"
isFirst={ false }
isLast={ true }
isLoading={ isLoading }
isSuccess={ false }
/>
</AccordionItem>
) }
</AccordionRoot>
);
};
......
import { HStack } from '@chakra-ui/react';
import { Box, HStack, Spinner } from '@chakra-ui/react';
import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types';
......@@ -9,7 +9,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
status: tac.OperationStage_StageType;
status: tac.OperationStage_StageType | 'pending';
isFirst: boolean;
isLast: boolean;
isLoading?: boolean;
......@@ -17,6 +17,32 @@ interface Props {
}
const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, isSuccess, isLoading }: Props) => {
const content = (() => {
switch (status) {
case 'pending': {
return (
<HStack gap={ 2 }>
<Spinner size="md"/>
<Box color="text.secondary">
Pending
</Box>
</HStack>
);
}
default: {
return (
<HStack gap={ 2 } color={ isSuccess ? 'green.500' : 'red.600' }>
<IconSvg name={ isSuccess ? 'verification-steps/finalized' : 'verification-steps/error' } boxSize={ 5 } isLoading={ isLoading }/>
<Skeleton loading={ isLoading }>
{ STATUS_LABELS[status] }
</Skeleton>
</HStack>
);
}
}
})();
return (
<AccordionItemTrigger
position="relative"
......@@ -47,15 +73,14 @@ const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, is
height: { base: '14px', lg: '6px' },
},
}}
disabled={ isLoading }
noIndicator={ isLoading }
disabled={ isLoading || status === 'pending' }
noIndicator={ isLoading || status === 'pending' }
cursor={ status === 'pending' ? 'default' : 'pointer' }
_disabled={{
opacity: status === 'pending' ? 1 : 'control.disabled',
}}
>
<HStack gap={ 2 } color={ isSuccess ? 'green.500' : 'red.600' }>
<IconSvg name={ isSuccess ? 'verification-steps/finalized' : 'verification-steps/error' } boxSize={ 5 } isLoading={ isLoading }/>
<Skeleton loading={ isLoading }>
{ STATUS_LABELS[status] }
</Skeleton>
</HStack>
{ content }
</AccordionItemTrigger>
);
};
......
......@@ -18,6 +18,7 @@ const TacOperationsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Value>
<OperationEntity
id={ item.operation_id }
type={ item.type }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
......
......@@ -19,12 +19,12 @@ const TacOperationsTable = ({ items, isLoading }: Props) => {
<TableRoot minW="950px">
<TableHeaderSticky top={ 68 }>
<TableRow>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="100%">Operation</TableColumnHeader>
<TableColumnHeader w="200px">
Timestamp
<TimeFormatToggle/>
</TableColumnHeader>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="250px">Sender</TableColumnHeader>
</TableRow>
</TableHeaderSticky>
......
......@@ -15,10 +15,15 @@ interface Props {
const TacOperationsTableItem = ({ item, isLoading }: Props) => {
return (
<TableRow>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle">
<OperationEntity
id={ item.operation_id }
type={ item.type }
isLoading={ isLoading }
truncation="constant_long"
/>
</TableCell>
<TableCell verticalAlign="middle">
......@@ -28,9 +33,6 @@ const TacOperationsTableItem = ({ item, isLoading }: Props) => {
color="text.secondary"
/>
</TableCell>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle" pr={ 12 }>
{ item.sender ? (
<AddressEntityTacTon
......
......@@ -47,7 +47,7 @@ const RewardsDashboard = () => {
return (
<>
<Flex gap={ 3 } justifyContent="space-between">
<Flex gap={ 3 } justifyContent="space-between" mb={ 6 }>
<PageTitle
title="Dashboard"
secondRow={ (
......@@ -58,6 +58,7 @@ const RewardsDashboard = () => {
to earn, spend, and learn more about the program.
</span>
) }
mb={ 0 }
/>
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex>
......
import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import * as tacOperationMock from 'mocks/operations/tac';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......@@ -27,3 +29,25 @@ test('base view +@dark-mode +@mobile', async({ render, mockTextAd, mockApiRespon
await component.getByRole('button', { name: 'Executed in TON' }).click();
await expect(component).toHaveScreenshot();
});
test('pending operation', async({ render, mockTextAd, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.tac);
await mockTextAd();
await mockApiResponse('tac:operation', {
... tacOperationMock.tacOperation,
type: tac.OperationType.PENDING,
}, {
pathParams: { id: tacOperationMock.tacOperation.operation_id },
});
const component = await render(
<TacOperation/>,
{ hooksConfig: {
router: {
query: { id: tacOperationMock.tacOperation.operation_id },
isReady: true,
},
} },
);
await expect(component).toHaveScreenshot();
});
......@@ -44,7 +44,7 @@ const TacOperation = () => {
) : null;
const titleSecondRow = (
<OperationEntity id={ id } noLink variant="subheading"/>
<OperationEntity id={ id } noLink variant="subheading" type={ query.data?.type }/>
);
return (
......
......@@ -32,7 +32,7 @@ import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TacOperationTag from 'ui/shared/TacOperationTag';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import SearchResultEntityTag from './SearchResultEntityTag';
......@@ -219,7 +219,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
case 'tac_operation': {
return (
<OperationEntity.Container>
<OperationEntity.Icon/>
<OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link
isLoading={ isLoading }
id={ data.tac_operation.operation_id }
......@@ -233,7 +233,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
mr={ 2 }
/>
</OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/>
<TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container>
);
}
......
......@@ -32,7 +32,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TacOperationTag from 'ui/shared/TacOperationTag';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import SearchResultEntityTag from './SearchResultEntityTag';
......@@ -331,7 +331,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
<>
<TableCell colSpan={ 2 } fontSize="sm">
<OperationEntity.Container>
<OperationEntity.Icon/>
<OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link
isLoading={ isLoading }
id={ data.tac_operation.operation_id }
......@@ -345,7 +345,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
mr={ 2 }
/>
</OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/>
<TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container>
</TableCell>
<TableCell fontSize="sm" verticalAlign="middle" isNumeric>
......
import { Flex, chakra } from '@chakra-ui/react';
import { debounce } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
......@@ -29,10 +31,12 @@ const TEXT_MAX_LINES = 1;
const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading = false, afterTitle, beforeTitle, secondRow }: Props) => {
const tooltip = useDisclosure();
const isMobile = useIsMobile();
const router = useRouter();
const [ isTextTruncated, setIsTextTruncated ] = React.useState(false);
const headingRef = React.useRef<HTMLHeadingElement>(null);
const textRef = React.useRef<HTMLSpanElement>(null);
const pageType = mixpanel.getPageType(router.pathname);
const updatedTruncateState = React.useCallback(() => {
if (!headingRef.current || !textRef.current) {
......@@ -71,6 +75,11 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
}
}, [ tooltip ]);
const handleBackToClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Back to', Source: pageType });
backLink && 'onClick' in backLink && backLink.onClick();
}, [ backLink, pageType ]);
return (
<Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }>
<Flex
......@@ -85,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
<BackToButton
hint={ backLink.label }
href={ 'url' in backLink ? backLink.url : undefined }
onClick={ 'onClick' in backLink ? backLink.onClick : undefined }
onClick={ handleBackToClick }
loadingSkeleton={ isLoading }
mr={ 3 }
/>
......
......@@ -68,7 +68,7 @@ const Icon = (props: IconProps) => {
const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + props.hintPostfix;
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + (props.hintPostfix ?? '');
return (
<EntityBase.Icon
......@@ -83,7 +83,7 @@ const Icon = (props: IconProps) => {
const label = (() => {
if (isDelegatedAddress) {
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + props.hintPostfix;
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + (props.hintPostfix ?? '');
}
return props.hint;
......
......@@ -23,7 +23,7 @@ const AddressEntityTacTon = (props: Props) => {
const href = (() => {
switch (props.chainType) {
case tac.BlockchainType.TON:
return tacFeature.explorerUrl + route({
return tacFeature.tonExplorerUrl + route({
pathname: '/address/[hash]',
query: {
...props.query,
......
import { chakra } from '@chakra-ui/react';
import { Spinner, chakra } from '@chakra-ui/react';
import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
......@@ -22,7 +24,14 @@ const Link = chakra((props: LinkProps) => {
);
});
const Icon = (props: EntityBase.IconBaseProps) => {
type IconProps = EntityBase.IconBaseProps & Pick<EntityProps, 'type'>;
const Icon = (props: IconProps) => {
switch (props.type) {
case tac.OperationType.PENDING: {
return <Spinner size="md" marginRight={ props.marginRight ?? '8px' }/>;
}
default: {
return (
<EntityBase.Icon
{ ...props }
......@@ -30,6 +39,8 @@ const Icon = (props: EntityBase.IconBaseProps) => {
borderRadius="none"
/>
);
}
}
};
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>;
......@@ -58,6 +69,7 @@ const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps {
id: string;
type: tac.OperationType | undefined;
}
const OperationEntity = (props: EntityProps) => {
......
......@@ -14,7 +14,7 @@ const TxEntityTon = (props: TxEntity.EntityProps) => {
}
const formattedHash = props.hash.replace(/^0x/, '');
const defaultHref = `${ stripTrailingSlash(tacFeature.explorerUrl) }/tx/${ formattedHash }`;
const defaultHref = `${ stripTrailingSlash(tacFeature.tonExplorerUrl) }/transaction/${ formattedHash }`;
return <TxEntity.default { ...props } hash={ formattedHash } href={ props.href ?? defaultHref } icon={{ name: 'brands/ton' }} isExternal/>;
};
......
import React from 'react';
import { useSettingsContext } from 'lib/contexts/settings';
import * as mixpanel from 'lib/mixpanel/index';
import { IconButton } from 'toolkit/chakra/icon-button';
import type { IconButtonProps } from 'toolkit/chakra/icon-button';
import { Tooltip } from 'toolkit/chakra/tooltip';
......@@ -12,12 +13,17 @@ const TimeFormatToggle = (props: Props) => {
const settings = useSettingsContext();
const timeFormat = settings?.timeFormat || 'relative';
const handleClick = React.useCallback(() => {
settings?.toggleTimeFormat();
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Switch time format', Source: 'Table header' });
}, [ settings ]);
return (
<Tooltip content="Toggle time format">
<IconButton
aria-label="Toggle time format"
variant="icon_secondary"
onClick={ settings?.toggleTimeFormat }
onClick={ handleClick }
boxSize={ 5 }
selected={ timeFormat === 'absolute' }
borderRadius="sm"
......
......@@ -7,6 +7,7 @@ import { NETWORK_GROUPS } from 'types/networks';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
import * as mixpanel from 'lib/mixpanel/index';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
export default function useNetworkMenu() {
......@@ -20,14 +21,21 @@ export default function useNetworkMenu() {
staleTime: Infinity,
});
const handleOpenChange = React.useCallback((details: { open: boolean }) => {
if (details.open) {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Network menu', Source: 'Header' });
}
onOpenChange(details);
}, [ onOpenChange ]);
return React.useMemo(() => ({
open,
onClose,
onOpen,
onToggle,
onOpenChange,
onOpenChange: handleOpenChange,
isPending,
data,
availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)),
}), [ open, onClose, onOpen, onToggle, onOpenChange, data, isPending ]);
}), [ open, onClose, onOpen, onToggle, handleOpenChange, data, isPending ]);
}
......@@ -7,16 +7,16 @@ import type { SearchResultTacOperation } from 'types/api/search';
import dayjs from 'lib/date/dayjs';
import * as OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TacOperationTag from 'ui/shared/TacOperationTag';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
const SearchBarSuggestTacOperation = ({ data, isMobile }: ItemsProps<SearchResultTacOperation>) => {
const icon = <OperationEntity.Icon/>;
const icon = <OperationEntity.Icon type={ data.tac_operation.type }/>;
const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 } mr={ 2 }>
<HashStringShortenDynamic hash={ data.tac_operation.operation_id } noTooltip/>
</chakra.mark>
);
const status = <TacOperationTag type={ data.tac_operation.type }/>;
const status = <TacOperationStatus status={ data.tac_operation.type }/>;
const date = dayjs(data.tac_operation.timestamp).format('llll');
if (isMobile) {
......
......@@ -3,10 +3,11 @@ import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types';
import { getTacOperationStatus, getTacOperationStage } from 'lib/operations/tac';
import { getTacOperationStage } from 'lib/operations/tac';
import { Tag } from 'toolkit/chakra/tag';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
interface Props {
tacOperations: Array<tac.OperationDetails>;
......@@ -35,19 +36,20 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => {
>
{ tacOperations.map((tacOperation) => {
const tags = [
getTacOperationStage(tacOperation, txHash),
getTacOperationStatus(tacOperation.type),
...(getTacOperationStage(tacOperation, txHash) || []),
];
return (
<HStack key={ tacOperation.operation_id } gap={ 3 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<OperationEntity
id={ tacOperation.operation_id }
type={ tacOperation.type }
isLoading={ isLoading }
/>
{ tags.length > 0 && (
<HStack flexShrink={ 0 }>
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading }>{ tag }</Tag>) }
<HStack flexShrink={ 0 } flexWrap="wrap">
<TacOperationStatus status={ tacOperation.type } isLoading={ isLoading }/>
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading } flexShrink={ 0 }>{ tag }</Tag>) }
</HStack>
) }
</HStack>
......
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