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)'; ...@@ -7,12 +7,12 @@ const title = 'Ton Application Chain (TAC)';
const tonExplorerUrl = getEnvValue('NEXT_PUBLIC_TAC_TON_EXPLORER_URL'); const tonExplorerUrl = getEnvValue('NEXT_PUBLIC_TAC_TON_EXPLORER_URL');
const config: Feature<{ explorerUrl: string }> = (() => { const config: Feature<{ tonExplorerUrl: string }> = (() => {
if (apis.tac && tonExplorerUrl) { if (apis.tac && tonExplorerUrl) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
explorerUrl: tonExplorerUrl, tonExplorerUrl,
}); });
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=tac_turin" # 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_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 # Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http NEXT_PUBLIC_APP_PROTOCOL=http
......
This diff is collapsed.
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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="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"/>
<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"/>
</svg> </svg>
import getPageType from './getPageType'; import getPageType, { PAGE_TYPE_DICT } from './getPageType';
import logEvent from './logEvent'; import logEvent from './logEvent';
import reset from './reset'; import reset from './reset';
import useInit from './useInit'; import useInit from './useInit';
...@@ -13,4 +13,5 @@ export { ...@@ -13,4 +13,5 @@ export {
getPageType, getPageType,
userProfile, userProfile,
reset, reset,
PAGE_TYPE_DICT,
}; };
...@@ -22,11 +22,11 @@ export function getTacOperationStatus(type: tac.OperationType) { ...@@ -22,11 +22,11 @@ export function getTacOperationStatus(type: tac.OperationType) {
} }
export function getTacOperationStage(data: tac.OperationDetails, txHash: string) { export function getTacOperationStage(data: tac.OperationDetails, txHash: string) {
const currentStep = data.status_history.find((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase())); const currentStep = data.status_history.filter((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase()));
if (!currentStep) { if (currentStep.length === 0) {
return null; return;
} }
return STATUS_LABELS[currentStep.type]; return currentStep.map((step) => STATUS_LABELS[step.type]);
} }
export const STATUS_SEQUENCE: Array<tac.OperationStage_StageType> = [ export const STATUS_SEQUENCE: Array<tac.OperationStage_StageType> = [
......
...@@ -107,6 +107,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -107,6 +107,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
], ],
tac: [ tac: [
[ 'NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ], [ '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'; ...@@ -4,7 +4,7 @@ import { ADDRESS_HASH } from './addressParams';
export const TAC_OPERATION: tac.OperationBriefDetails = { export const TAC_OPERATION: tac.OperationBriefDetails = {
operation_id: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6', operation_id: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
type: tac.OperationType.PENDING, type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z', timestamp: '2025-05-05T12:32:22.000Z',
sender: { sender: {
address: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6', address: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
...@@ -14,7 +14,7 @@ export const TAC_OPERATION: tac.OperationBriefDetails = { ...@@ -14,7 +14,7 @@ export const TAC_OPERATION: tac.OperationBriefDetails = {
export const TAC_OPERATION_DETAILS: tac.OperationDetails = { export const TAC_OPERATION_DETAILS: tac.OperationDetails = {
operation_id: '0x6e7cdeea3f39e7664597a44ddb33ce47ba061cbee2992e2c7b0e3f9294ff8b30', operation_id: '0x6e7cdeea3f39e7664597a44ddb33ce47ba061cbee2992e2c7b0e3f9294ff8b30',
type: tac.OperationType.PENDING, type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z', timestamp: '2025-05-05T12:32:22.000Z',
sender: { sender: {
address: ADDRESS_HASH, address: ADDRESS_HASH,
......
...@@ -32,6 +32,10 @@ type Props = { ...@@ -32,6 +32,10 @@ type Props = {
const DateInput = ({ value, onChange, placeholder, max }: { value: string; onChange: (value: string) => void; placeholder: string; max: string }) => { 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') : ''); 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>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setTempValue(event.target.value); setTempValue(event.target.value);
onChange(event.target.value); onChange(event.target.value);
...@@ -96,7 +100,7 @@ const AgeFilter = ({ value = defaultValue, handleFilterChange, onClose }: Props) ...@@ -96,7 +100,7 @@ const AgeFilter = ({ value = defaultValue, handleFilterChange, onClose }: Props)
<TableColumnFilter <TableColumnFilter
title="Set last duration" title="Set last duration"
isFilled={ Boolean(currentValue.from || currentValue.to || currentValue.age) } 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 } onFilter={ onFilter }
onReset={ onReset } onReset={ onReset }
hasReset hasReset
......
...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes'; ...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import { BackToButton } from 'toolkit/components/buttons/BackToButton'; import { BackToButton } from 'toolkit/components/buttons/BackToButton';
import { makePrettyLink } from 'toolkit/utils/url'; import { makePrettyLink } from 'toolkit/utils/url';
...@@ -46,6 +47,10 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) ...@@ -46,6 +47,10 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []); const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []);
const hideContractList = React.useCallback(() => setContractListType(undefined), []); 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 ( return (
<> <>
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }> <Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
...@@ -54,6 +59,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) ...@@ -54,6 +59,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
href={ goBackUrl } href={ goBackUrl }
hint="Back to dApps list" hint="Back to dApps list"
loading={ isLoading } loading={ isLoading }
onClick={ handleBackToClick }
/> />
<Link <Link
external external
......
...@@ -7,6 +7,7 @@ import { sortStatusHistory } from 'lib/operations/tac'; ...@@ -7,6 +7,7 @@ import { sortStatusHistory } from 'lib/operations/tac';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp'; import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp';
import AddressEntityTacTon from 'ui/shared/entities/address/AddressEntityTacTon'; import AddressEntityTacTon from 'ui/shared/entities/address/AddressEntityTacTon';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import TacOperationLifecycleAccordion from './TacOperationLifecycleAccordion'; import TacOperationLifecycleAccordion from './TacOperationLifecycleAccordion';
...@@ -43,6 +44,16 @@ const TacOperationDetails = ({ isLoading, data }: Props) => { ...@@ -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 && ( { data.timestamp && (
<> <>
<DetailedInfo.ItemLabel <DetailedInfo.ItemLabel
...@@ -66,7 +77,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => { ...@@ -66,7 +77,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => {
Lifecycle Lifecycle
</DetailedInfo.ItemLabel> </DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue> <DetailedInfo.ItemValue>
<TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading }/> <TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading } type={ data.type }/>
</DetailedInfo.ItemValue> </DetailedInfo.ItemValue>
</> </>
) } ) }
......
import React from 'react'; 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'; import { AccordionItem, AccordionRoot } from 'toolkit/chakra/accordion';
...@@ -10,13 +10,16 @@ import TacOperationLifecycleAccordionItemTrigger from './TacOperationLifecycleAc ...@@ -10,13 +10,16 @@ import TacOperationLifecycleAccordionItemTrigger from './TacOperationLifecycleAc
interface Props { interface Props {
data: tac.OperationDetails['status_history']; data: tac.OperationDetails['status_history'];
isLoading?: boolean; isLoading?: boolean;
type: tac.OperationType;
} }
const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => { const TacOperationLifecycleAccordion = ({ data, isLoading, type }: Props) => {
const isPending = type === tac.OperationType.PENDING && !isLoading;
return ( return (
<AccordionRoot maxW="800px" display="flex" flexDirection="column" rowGap={ 6 } lazyMount> <AccordionRoot maxW="800px" display="flex" flexDirection="column" rowGap={ 6 } lazyMount>
{ data.map((item, index) => { { data.map((item, index) => {
const isLast = index === data.length - 1; const isLast = index === data.length - 1 && !isPending;
return ( return (
<AccordionItem key={ index } value={ item.type } borderBottomWidth="0px"> <AccordionItem key={ index } value={ item.type } borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger <TacOperationLifecycleAccordionItemTrigger
...@@ -33,6 +36,17 @@ const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => { ...@@ -33,6 +36,17 @@ const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => {
</AccordionItem> </AccordionItem>
); );
}) } }) }
{ isPending && (
<AccordionItem value="pending" borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger
status="pending"
isFirst={ false }
isLast={ true }
isLoading={ isLoading }
isSuccess={ false }
/>
</AccordionItem>
) }
</AccordionRoot> </AccordionRoot>
); );
}; };
......
import { HStack } from '@chakra-ui/react'; import { Box, HStack, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types'; import type * as tac from '@blockscout/tac-operation-lifecycle-types';
...@@ -9,7 +9,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton'; ...@@ -9,7 +9,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
status: tac.OperationStage_StageType; status: tac.OperationStage_StageType | 'pending';
isFirst: boolean; isFirst: boolean;
isLast: boolean; isLast: boolean;
isLoading?: boolean; isLoading?: boolean;
...@@ -17,6 +17,32 @@ interface Props { ...@@ -17,6 +17,32 @@ interface Props {
} }
const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, isSuccess, isLoading }: 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 ( return (
<AccordionItemTrigger <AccordionItemTrigger
position="relative" position="relative"
...@@ -47,15 +73,14 @@ const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, is ...@@ -47,15 +73,14 @@ const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, is
height: { base: '14px', lg: '6px' }, height: { base: '14px', lg: '6px' },
}, },
}} }}
disabled={ isLoading } disabled={ isLoading || status === 'pending' }
noIndicator={ isLoading } 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' }> { content }
<IconSvg name={ isSuccess ? 'verification-steps/finalized' : 'verification-steps/error' } boxSize={ 5 } isLoading={ isLoading }/>
<Skeleton loading={ isLoading }>
{ STATUS_LABELS[status] }
</Skeleton>
</HStack>
</AccordionItemTrigger> </AccordionItemTrigger>
); );
}; };
......
...@@ -18,6 +18,7 @@ const TacOperationsListItem = ({ item, isLoading }: Props) => { ...@@ -18,6 +18,7 @@ const TacOperationsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<OperationEntity <OperationEntity
id={ item.operation_id } id={ item.operation_id }
type={ item.type }
isLoading={ isLoading } isLoading={ isLoading }
/> />
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
......
...@@ -19,12 +19,12 @@ const TacOperationsTable = ({ items, isLoading }: Props) => { ...@@ -19,12 +19,12 @@ const TacOperationsTable = ({ items, isLoading }: Props) => {
<TableRoot minW="950px"> <TableRoot minW="950px">
<TableHeaderSticky top={ 68 }> <TableHeaderSticky top={ 68 }>
<TableRow> <TableRow>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="100%">Operation</TableColumnHeader> <TableColumnHeader w="100%">Operation</TableColumnHeader>
<TableColumnHeader w="200px"> <TableColumnHeader w="200px">
Timestamp Timestamp
<TimeFormatToggle/> <TimeFormatToggle/>
</TableColumnHeader> </TableColumnHeader>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="250px">Sender</TableColumnHeader> <TableColumnHeader w="250px">Sender</TableColumnHeader>
</TableRow> </TableRow>
</TableHeaderSticky> </TableHeaderSticky>
......
...@@ -15,10 +15,15 @@ interface Props { ...@@ -15,10 +15,15 @@ interface Props {
const TacOperationsTableItem = ({ item, isLoading }: Props) => { const TacOperationsTableItem = ({ item, isLoading }: Props) => {
return ( return (
<TableRow> <TableRow>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle"> <TableCell verticalAlign="middle">
<OperationEntity <OperationEntity
id={ item.operation_id } id={ item.operation_id }
type={ item.type }
isLoading={ isLoading } isLoading={ isLoading }
truncation="constant_long"
/> />
</TableCell> </TableCell>
<TableCell verticalAlign="middle"> <TableCell verticalAlign="middle">
...@@ -28,9 +33,6 @@ const TacOperationsTableItem = ({ item, isLoading }: Props) => { ...@@ -28,9 +33,6 @@ const TacOperationsTableItem = ({ item, isLoading }: Props) => {
color="text.secondary" color="text.secondary"
/> />
</TableCell> </TableCell>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle" pr={ 12 }> <TableCell verticalAlign="middle" pr={ 12 }>
{ item.sender ? ( { item.sender ? (
<AddressEntityTacTon <AddressEntityTacTon
......
...@@ -47,7 +47,7 @@ const RewardsDashboard = () => { ...@@ -47,7 +47,7 @@ const RewardsDashboard = () => {
return ( return (
<> <>
<Flex gap={ 3 } justifyContent="space-between"> <Flex gap={ 3 } justifyContent="space-between" mb={ 6 }>
<PageTitle <PageTitle
title="Dashboard" title="Dashboard"
secondRow={ ( secondRow={ (
...@@ -58,6 +58,7 @@ const RewardsDashboard = () => { ...@@ -58,6 +58,7 @@ const RewardsDashboard = () => {
to earn, spend, and learn more about the program. to earn, spend, and learn more about the program.
</span> </span>
) } ) }
mb={ 0 }
/> />
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/> <AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex> </Flex>
......
import React from 'react'; import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import * as tacOperationMock from 'mocks/operations/tac'; import * as tacOperationMock from 'mocks/operations/tac';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
...@@ -27,3 +29,25 @@ test('base view +@dark-mode +@mobile', async({ render, mockTextAd, mockApiRespon ...@@ -27,3 +29,25 @@ test('base view +@dark-mode +@mobile', async({ render, mockTextAd, mockApiRespon
await component.getByRole('button', { name: 'Executed in TON' }).click(); await component.getByRole('button', { name: 'Executed in TON' }).click();
await expect(component).toHaveScreenshot(); 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 = () => { ...@@ -44,7 +44,7 @@ const TacOperation = () => {
) : null; ) : null;
const titleSecondRow = ( const titleSecondRow = (
<OperationEntity id={ id } noLink variant="subheading"/> <OperationEntity id={ id } noLink variant="subheading" type={ query.data?.type }/>
); );
return ( return (
......
...@@ -32,7 +32,7 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -32,7 +32,7 @@ import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import type { SearchResultAppItem } from 'ui/shared/search/utils'; import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } 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'; import SearchResultEntityTag from './SearchResultEntityTag';
...@@ -219,7 +219,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -219,7 +219,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
case 'tac_operation': { case 'tac_operation': {
return ( return (
<OperationEntity.Container> <OperationEntity.Container>
<OperationEntity.Icon/> <OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link <OperationEntity.Link
isLoading={ isLoading } isLoading={ isLoading }
id={ data.tac_operation.operation_id } id={ data.tac_operation.operation_id }
...@@ -233,7 +233,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -233,7 +233,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
mr={ 2 } mr={ 2 }
/> />
</OperationEntity.Link> </OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/> <TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container> </OperationEntity.Container>
); );
} }
......
...@@ -32,7 +32,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -32,7 +32,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { SearchResultAppItem } from 'ui/shared/search/utils'; import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } 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'; import SearchResultEntityTag from './SearchResultEntityTag';
...@@ -331,7 +331,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -331,7 +331,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
<> <>
<TableCell colSpan={ 2 } fontSize="sm"> <TableCell colSpan={ 2 } fontSize="sm">
<OperationEntity.Container> <OperationEntity.Container>
<OperationEntity.Icon/> <OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link <OperationEntity.Link
isLoading={ isLoading } isLoading={ isLoading }
id={ data.tac_operation.operation_id } id={ data.tac_operation.operation_id }
...@@ -345,7 +345,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -345,7 +345,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
mr={ 2 } mr={ 2 }
/> />
</OperationEntity.Link> </OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/> <TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container> </OperationEntity.Container>
</TableCell> </TableCell>
<TableCell fontSize="sm" verticalAlign="middle" isNumeric> <TableCell fontSize="sm" verticalAlign="middle" isNumeric>
......
import { Flex, chakra } from '@chakra-ui/react'; import { Flex, chakra } from '@chakra-ui/react';
import { debounce } from 'es-toolkit'; import { debounce } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Heading } from 'toolkit/chakra/heading'; import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
...@@ -29,10 +31,12 @@ const TEXT_MAX_LINES = 1; ...@@ -29,10 +31,12 @@ const TEXT_MAX_LINES = 1;
const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading = false, afterTitle, beforeTitle, secondRow }: Props) => { const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading = false, afterTitle, beforeTitle, secondRow }: Props) => {
const tooltip = useDisclosure(); const tooltip = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter();
const [ isTextTruncated, setIsTextTruncated ] = React.useState(false); const [ isTextTruncated, setIsTextTruncated ] = React.useState(false);
const headingRef = React.useRef<HTMLHeadingElement>(null); const headingRef = React.useRef<HTMLHeadingElement>(null);
const textRef = React.useRef<HTMLSpanElement>(null); const textRef = React.useRef<HTMLSpanElement>(null);
const pageType = mixpanel.getPageType(router.pathname);
const updatedTruncateState = React.useCallback(() => { const updatedTruncateState = React.useCallback(() => {
if (!headingRef.current || !textRef.current) { if (!headingRef.current || !textRef.current) {
...@@ -71,6 +75,11 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -71,6 +75,11 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
} }
}, [ tooltip ]); }, [ 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 ( return (
<Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }> <Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }>
<Flex <Flex
...@@ -85,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -85,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
<BackToButton <BackToButton
hint={ backLink.label } hint={ backLink.label }
href={ 'url' in backLink ? backLink.url : undefined } href={ 'url' in backLink ? backLink.url : undefined }
onClick={ 'onClick' in backLink ? backLink.onClick : undefined } onClick={ handleBackToClick }
loadingSkeleton={ isLoading } loadingSkeleton={ isLoading }
mr={ 3 } mr={ 3 }
/> />
......
...@@ -68,7 +68,7 @@ const Icon = (props: IconProps) => { ...@@ -68,7 +68,7 @@ const Icon = (props: IconProps) => {
const isProxy = Boolean(props.address.implementations?.length); 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 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 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 ( return (
<EntityBase.Icon <EntityBase.Icon
...@@ -83,7 +83,7 @@ const Icon = (props: IconProps) => { ...@@ -83,7 +83,7 @@ const Icon = (props: IconProps) => {
const label = (() => { const label = (() => {
if (isDelegatedAddress) { 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; return props.hint;
......
...@@ -23,7 +23,7 @@ const AddressEntityTacTon = (props: Props) => { ...@@ -23,7 +23,7 @@ const AddressEntityTacTon = (props: Props) => {
const href = (() => { const href = (() => {
switch (props.chainType) { switch (props.chainType) {
case tac.BlockchainType.TON: case tac.BlockchainType.TON:
return tacFeature.explorerUrl + route({ return tacFeature.tonExplorerUrl + route({
pathname: '/address/[hash]', pathname: '/address/[hash]',
query: { query: {
...props.query, ...props.query,
......
import { chakra } from '@chakra-ui/react'; import { Spinner, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components'; import * as EntityBase from 'ui/shared/entities/base/components';
...@@ -22,14 +24,23 @@ const Link = chakra((props: LinkProps) => { ...@@ -22,14 +24,23 @@ const Link = chakra((props: LinkProps) => {
); );
}); });
const Icon = (props: EntityBase.IconBaseProps) => { type IconProps = EntityBase.IconBaseProps & Pick<EntityProps, 'type'>;
return (
<EntityBase.Icon const Icon = (props: IconProps) => {
{ ...props } switch (props.type) {
name={ props.name ?? 'operation_slim' } case tac.OperationType.PENDING: {
borderRadius="none" return <Spinner size="md" marginRight={ props.marginRight ?? '8px' }/>;
/> }
); default: {
return (
<EntityBase.Icon
{ ...props }
name={ props.name ?? 'operation_slim' }
borderRadius="none"
/>
);
}
}
}; };
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>; type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>;
...@@ -58,6 +69,7 @@ const Container = EntityBase.Container; ...@@ -58,6 +69,7 @@ const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps { export interface EntityProps extends EntityBase.EntityBaseProps {
id: string; id: string;
type: tac.OperationType | undefined;
} }
const OperationEntity = (props: EntityProps) => { const OperationEntity = (props: EntityProps) => {
......
...@@ -14,7 +14,7 @@ const TxEntityTon = (props: TxEntity.EntityProps) => { ...@@ -14,7 +14,7 @@ const TxEntityTon = (props: TxEntity.EntityProps) => {
} }
const formattedHash = props.hash.replace(/^0x/, ''); 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/>; return <TxEntity.default { ...props } hash={ formattedHash } href={ props.href ?? defaultHref } icon={{ name: 'brands/ton' }} isExternal/>;
}; };
......
import React from 'react'; import React from 'react';
import { useSettingsContext } from 'lib/contexts/settings'; import { useSettingsContext } from 'lib/contexts/settings';
import * as mixpanel from 'lib/mixpanel/index';
import { IconButton } from 'toolkit/chakra/icon-button'; import { IconButton } from 'toolkit/chakra/icon-button';
import type { IconButtonProps } from 'toolkit/chakra/icon-button'; import type { IconButtonProps } from 'toolkit/chakra/icon-button';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
...@@ -12,12 +13,17 @@ const TimeFormatToggle = (props: Props) => { ...@@ -12,12 +13,17 @@ const TimeFormatToggle = (props: Props) => {
const settings = useSettingsContext(); const settings = useSettingsContext();
const timeFormat = settings?.timeFormat || 'relative'; 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 ( return (
<Tooltip content="Toggle time format"> <Tooltip content="Toggle time format">
<IconButton <IconButton
aria-label="Toggle time format" aria-label="Toggle time format"
variant="icon_secondary" variant="icon_secondary"
onClick={ settings?.toggleTimeFormat } onClick={ handleClick }
boxSize={ 5 } boxSize={ 5 }
selected={ timeFormat === 'absolute' } selected={ timeFormat === 'absolute' }
borderRadius="sm" borderRadius="sm"
......
...@@ -7,6 +7,7 @@ import { NETWORK_GROUPS } from 'types/networks'; ...@@ -7,6 +7,7 @@ import { NETWORK_GROUPS } from 'types/networks';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import * as mixpanel from 'lib/mixpanel/index';
import { useDisclosure } from 'toolkit/hooks/useDisclosure'; import { useDisclosure } from 'toolkit/hooks/useDisclosure';
export default function useNetworkMenu() { export default function useNetworkMenu() {
...@@ -20,14 +21,21 @@ export default function useNetworkMenu() { ...@@ -20,14 +21,21 @@ export default function useNetworkMenu() {
staleTime: Infinity, 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(() => ({ return React.useMemo(() => ({
open, open,
onClose, onClose,
onOpen, onOpen,
onToggle, onToggle,
onOpenChange, onOpenChange: handleOpenChange,
isPending, isPending,
data, data,
availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)), 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'; ...@@ -7,16 +7,16 @@ import type { SearchResultTacOperation } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import * as OperationEntity from 'ui/shared/entities/operation/OperationEntity'; import * as OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; 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 SearchBarSuggestTacOperation = ({ data, isMobile }: ItemsProps<SearchResultTacOperation>) => {
const icon = <OperationEntity.Icon/>; const icon = <OperationEntity.Icon type={ data.tac_operation.type }/>;
const hash = ( const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 } mr={ 2 }> <chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 } mr={ 2 }>
<HashStringShortenDynamic hash={ data.tac_operation.operation_id } noTooltip/> <HashStringShortenDynamic hash={ data.tac_operation.operation_id } noTooltip/>
</chakra.mark> </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'); const date = dayjs(data.tac_operation.timestamp).format('llll');
if (isMobile) { if (isMobile) {
......
...@@ -3,10 +3,11 @@ import React from 'react'; ...@@ -3,10 +3,11 @@ import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types'; 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 { Tag } from 'toolkit/chakra/tag';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import OperationEntity from 'ui/shared/entities/operation/OperationEntity'; import OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
interface Props { interface Props {
tacOperations: Array<tac.OperationDetails>; tacOperations: Array<tac.OperationDetails>;
...@@ -35,19 +36,20 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => { ...@@ -35,19 +36,20 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => {
> >
{ tacOperations.map((tacOperation) => { { tacOperations.map((tacOperation) => {
const tags = [ const tags = [
getTacOperationStage(tacOperation, txHash), ...(getTacOperationStage(tacOperation, txHash) || []),
getTacOperationStatus(tacOperation.type),
]; ];
return ( return (
<HStack key={ tacOperation.operation_id } gap={ 3 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}> <HStack key={ tacOperation.operation_id } gap={ 3 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<OperationEntity <OperationEntity
id={ tacOperation.operation_id } id={ tacOperation.operation_id }
type={ tacOperation.type }
isLoading={ isLoading } isLoading={ isLoading }
/> />
{ tags.length > 0 && ( { tags.length > 0 && (
<HStack flexShrink={ 0 }> <HStack flexShrink={ 0 } flexWrap="wrap">
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading }>{ tag }</Tag>) } <TacOperationStatus status={ tacOperation.type } isLoading={ isLoading }/>
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading } flexShrink={ 0 }>{ tag }</Tag>) }
</HStack> </HStack>
) } ) }
</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