Commit 3bbeec15 authored by tom's avatar tom

tac ui updates

parent ab048643
...@@ -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,
......
...@@ -66,7 +66,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => { ...@@ -66,7 +66,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
......
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 (
......
...@@ -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 }
......
...@@ -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 }
......
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,40 @@ const Link = chakra((props: LinkProps) => { ...@@ -22,14 +24,40 @@ 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: {
const color = (() => {
switch (props.type) {
case tac.OperationType.ERROR:
case tac.OperationType.ROLLBACK: {
return 'red.500';
}
case tac.OperationType.TAC_TON:
case tac.OperationType.TON_TAC_TON:
case tac.OperationType.TON_TAC: {
return 'green.500';
}
default:
return;
}
})();
return (
<EntityBase.Icon
{ ...props }
color={ color }
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 +86,7 @@ const Container = EntityBase.Container; ...@@ -58,6 +86,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) => {
......
...@@ -10,7 +10,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -10,7 +10,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TacOperationTag from 'ui/shared/TacOperationTag'; import TacOperationTag from 'ui/shared/TacOperationTag';
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/>
......
...@@ -43,6 +43,7 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => { ...@@ -43,6 +43,7 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => {
<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 && (
......
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