Commit 957b2e7f authored by isstuev's avatar isstuev

design fixes and tests

parent a44d803f
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="#4A5568" fill-rule="evenodd" d="M7.904 22.831c.335.102.681.153 1.029.152a3.59 3.59 0 0 0 1.907-.583 4.14 4.14 0 0 0 1.17-1.155 4.666 4.666 0 0 0 .68-1.57l.165-.688h4.287l.128.704c.132.569.363 1.102.68 1.57.319.468.716.86 1.171 1.155a3.61 3.61 0 0 0 1.444.548 3.483 3.483 0 0 0 1.521-.117c1.011-.32 1.87-1.07 2.39-2.092.52-1.021.66-2.23.39-3.366L23.2 10.31a4.69 4.69 0 0 0-.682-1.569 4.16 4.16 0 0 0-1.169-1.156 3.61 3.61 0 0 0-1.444-.548 3.483 3.483 0 0 0-1.52.117 3.793 3.793 0 0 0-1.442.832 4.34 4.34 0 0 0-1.017 1.413H14.07a4.357 4.357 0 0 0-1.018-1.411 3.814 3.814 0 0 0-1.44-.834 3.483 3.483 0 0 0-1.521-.117 3.6 3.6 0 0 0-1.444.548 4.14 4.14 0 0 0-1.173 1.162 4.668 4.668 0 0 0-.678 1.58L5.132 17.39a4.944 4.944 0 0 0 .393 3.354c.518 1.017 1.372 1.767 2.379 2.088ZM9.36 8.967c.375-.24.798-.366 1.23-.368.216 0 .433.033.642.096.39.123.747.349 1.042.657.296.309.522.691.659 1.117l.171.527h3.787l.171-.527a2.88 2.88 0 0 1 .655-1.116c.294-.309.65-.534 1.039-.658a2.22 2.22 0 0 1 .963-.073 2.3 2.3 0 0 1 .916.345c.29.19.544.442.746.742.203.3.35.643.432 1.008l1.665 7.064a3.162 3.162 0 0 1-.237 2.16c-.33.657-.88 1.141-1.527 1.348-.314.095-.642.12-.964.073a2.298 2.298 0 0 1-.916-.345 2.675 2.675 0 0 1-.749-.742 3.016 3.016 0 0 1-.437-1.008l-.435-1.878h-6.43l-.444 1.878a2.982 2.982 0 0 1-.435 1.01 2.64 2.64 0 0 1-.75.74 2.28 2.28 0 0 1-.912.346 2.199 2.199 0 0 1-.96-.074c-.648-.209-1.196-.693-1.527-1.35a3.179 3.179 0 0 1-.245-2.158l1.664-7.064c.173-.736.6-1.365 1.186-1.75ZM20 12.287a.714.714 0 1 1-1.43.001.714.714 0 0 1 1.43 0ZM9.918 14.906a1.43 1.43 0 1 0 1.588-2.377 1.43 1.43 0 0 0-1.588 2.377Zm9.367.955a.714.714 0 1 0 0-1.429.714.714 0 0 0 0 1.43Zm-.714-2.143a.714.714 0 1 1-1.429 0 .714.714 0 0 1 1.429 0Zm2.143.714a.714.714 0 1 0 0-1.429.714.714 0 0 0 0 1.43Z" clip-rule="evenodd"/>
<path fill="#000" fill-opacity=".2" fill-rule="evenodd" d="M7.904 22.831c.335.102.681.153 1.029.152a3.59 3.59 0 0 0 1.907-.583 4.14 4.14 0 0 0 1.17-1.155 4.666 4.666 0 0 0 .68-1.57l.165-.688h4.287l.128.704c.132.569.363 1.102.68 1.57.319.468.716.86 1.171 1.155a3.61 3.61 0 0 0 1.444.548 3.483 3.483 0 0 0 1.521-.117c1.011-.32 1.87-1.07 2.39-2.092.52-1.021.66-2.23.39-3.366L23.2 10.31a4.69 4.69 0 0 0-.682-1.569 4.16 4.16 0 0 0-1.169-1.156 3.61 3.61 0 0 0-1.444-.548 3.483 3.483 0 0 0-1.52.117 3.793 3.793 0 0 0-1.442.832 4.34 4.34 0 0 0-1.017 1.413H14.07a4.357 4.357 0 0 0-1.018-1.411 3.814 3.814 0 0 0-1.44-.834 3.483 3.483 0 0 0-1.521-.117 3.6 3.6 0 0 0-1.444.548 4.14 4.14 0 0 0-1.173 1.162 4.668 4.668 0 0 0-.678 1.58L5.132 17.39a4.944 4.944 0 0 0 .393 3.354c.518 1.017 1.372 1.767 2.379 2.088ZM9.36 8.967c.375-.24.798-.366 1.23-.368.216 0 .433.033.642.096.39.123.747.349 1.042.657.296.309.522.691.659 1.117l.171.527h3.787l.171-.527a2.88 2.88 0 0 1 .655-1.116c.294-.309.65-.534 1.039-.658a2.22 2.22 0 0 1 .963-.073 2.3 2.3 0 0 1 .916.345c.29.19.544.442.746.742.203.3.35.643.432 1.008l1.665 7.064a3.162 3.162 0 0 1-.237 2.16c-.33.657-.88 1.141-1.527 1.348-.314.095-.642.12-.964.073a2.298 2.298 0 0 1-.916-.345 2.675 2.675 0 0 1-.749-.742 3.016 3.016 0 0 1-.437-1.008l-.435-1.878h-6.43l-.444 1.878a2.982 2.982 0 0 1-.435 1.01 2.64 2.64 0 0 1-.75.74 2.28 2.28 0 0 1-.912.346 2.199 2.199 0 0 1-.96-.074c-.648-.209-1.196-.693-1.527-1.35a3.179 3.179 0 0 1-.245-2.158l1.664-7.064c.173-.736.6-1.365 1.186-1.75ZM20 12.287a.714.714 0 1 1-1.43.001.714.714 0 0 1 1.43 0ZM9.918 14.906a1.43 1.43 0 1 0 1.588-2.377 1.43 1.43 0 0 0-1.588 2.377Zm9.367.955a.714.714 0 1 0 0-1.429.714.714 0 0 0 0 1.43Zm-.714-2.143a.714.714 0 1 1-1.429 0 .714.714 0 0 1 1.429 0Zm2.143.714a.714.714 0 1 0 0-1.429.714.714 0 0 0 0 1.43Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M6.485 23.997a4.23 4.23 0 0 0 1.234.183 4.31 4.31 0 0 0 2.29-.7c.545-.353 1.022-.824 1.403-1.386s.66-1.202.817-1.884l.197-.825h5.144l.155.844c.157.682.435 1.323.816 1.885s.859 1.032 1.404 1.385a4.334 4.334 0 0 0 1.733.658 4.18 4.18 0 0 0 1.825-.14c1.214-.384 2.244-1.285 2.868-2.51a5.933 5.933 0 0 0 .467-4.04l-1.997-8.496c-.16-.682-.437-1.322-.818-1.883s-.858-1.033-1.403-1.387a4.333 4.333 0 0 0-1.733-.657 4.18 4.18 0 0 0-1.825.14c-.632.194-1.221.534-1.73.998s-.923 1.042-1.22 1.696h-2.228c-.298-.653-.714-1.23-1.222-1.694s-1.096-.804-1.728-1a4.18 4.18 0 0 0-1.825-.14c-.61.088-1.2.312-1.733.657A4.969 4.969 0 0 0 5.97 7.095a5.602 5.602 0 0 0-.813 1.895l-1.998 8.477a5.932 5.932 0 0 0 .472 4.025c.621 1.22 1.646 2.12 2.855 2.505zM8.234 7.36a2.752 2.752 0 0 1 1.474-.441c.26 0 .52.04.772.115.467.148.895.418 1.25.788.355.37.626.83.79 1.34l.206.634h4.544l.206-.633c.163-.51.432-.97.786-1.34s.78-.64 1.246-.789c.377-.114.77-.144 1.156-.088.387.056.76.197 1.099.414.348.228.653.53.896.891.243.36.419.772.518 1.21l1.998 8.476c.214.872.112 1.804-.285 2.592s-1.055 1.37-1.833 1.618c-.376.114-.77.144-1.156.088a2.761 2.761 0 0 1-1.098-.414c-.35-.228-.655-.531-.9-.892s-.422-.77-.524-1.208l-.523-2.254H11.14l-.532 2.254c-.1.438-.277.85-.522 1.21-.244.361-.55.663-.9.89a2.738 2.738 0 0 1-1.095.414 2.64 2.64 0 0 1-1.152-.088c-.777-.25-1.435-.832-1.832-1.62a3.815 3.815 0 0 1-.294-2.59L6.81 9.46c.208-.883.72-1.638 1.424-2.1zm12.765 3.985a.857.857 0 1 1-1.714 0 .857.857 0 0 1 1.714 0zm-12.098 3.14a1.715 1.715 0 1 0 1.906-2.85 1.715 1.715 0 0 0-1.906 2.85zm11.241 1.147a.857.857 0 1 0 0-1.714.857.857 0 0 0 0 1.714zm-.857-2.572a.857.857 0 1 1-1.714 0 .857.857 0 0 1 1.714 0zm2.572.857a.857.857 0 1 0 0-1.714.857.857 0 0 0 0 1.714z" fill-rule="evenodd" clip-rule="evenodd"/>
</svg>
/* eslint-disable max-len */
import type { AddressMudRecord, AddressMudRecords, AddressMudRecordsItem, AddressMudTables } from 'types/api/address';
import type { MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds';
export const table1: MudWorldTable = {
table_full_name: 'tb.store.Tables',
table_id: '0x746273746f72650000000000000000005461626c657300000000000000000000',
table_name: 'Tables',
table_namespace: 'store',
table_type: 'onchain',
};
export const table2: MudWorldTable = {
table_full_name: 'ot.world.FunctionSignatur',
table_id: '0x6f74776f726c6400000000000000000046756e6374696f6e5369676e61747572',
table_name: 'FunctionSignatur',
table_namespace: 'world',
table_type: 'offchain',
};
export const schema1: MudWorldSchema = {
key_names: [ 'moduleAddress', 'argumentsHash' ],
key_types: [ 'address', 'bytes32' ],
value_names: [ 'fieldLayout', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ],
value_types: [ 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ],
};
export const schema2: MudWorldSchema = {
key_names: [],
key_types: [],
value_names: [ 'value' ],
value_types: [ 'address' ],
};
export const mudTables: AddressMudTables = {
items: [
{
table: table1,
schema: schema1,
},
{
table: table2,
schema: schema2,
},
],
next_page_params: {
items_count: 50,
table_id: '1',
},
};
const record: AddressMudRecordsItem = {
decoded: {
abiEncodedFieldNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000006706c617965720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000974696d657374616d700000000000000000000000000000000000000000000000',
abiEncodedKeyNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026964000000000000000000000000000000000000000000000000000000000000',
goldCosts: [ '100000', '150000', '200000', '250000', '400000', '550000', '700000' ],
prototypeIds: [
'0x53776f7264736d616e0000000000000000000000000000000000000000000000',
'0x50696b656d616e00000000000000000000000000000000000000000000000000',
'0x50696b656d616e00000000000000000000000000000000000000000000000000',
'0x4172636865720000000000000000000000000000000000000000000000000000',
'0x4b6e696768740000000000000000000000000000000000000000000000000000',
],
keySchema: '0x002001001f000000000000000000000000000000000000000000000000000000',
tableId: '0x6f74000000000000000000000000000044726177557064617465000000000000',
valueSchema: '0x00540300611f1f00000000000000000000000000000000000000000000000000',
},
id: '0x007a651a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007',
is_deleted: false,
timestamp: '2024-05-09T15:14:32.000000Z',
};
export const mudRecords: AddressMudRecords = {
items: [ record, record ],
next_page_params: {
items_count: 50,
key0: '1',
key1: '2',
key_bytes: '3',
},
schema: {
key_names: [ 'tableId' ],
key_types: [ 'bytes32' ],
value_names: [ 'prototypeIds', 'goldCosts', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ],
value_types: [ 'bytes32[]', 'int32[]', 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ],
},
table: table1,
};
export const mudRecord: AddressMudRecord = {
record,
schema: mudRecords.schema,
table: table1,
};
import type { MudWorldsResponse } from 'types/api/mudWorlds';
import { withName, withoutName } from 'mocks/address/address';
export const mudWorlds: MudWorldsResponse = {
items: [
{
address: withName,
coin_balance: '300000000000000000',
tx_count: 3938,
},
{
address: withoutName,
coin_balance: '0',
tx_count: 0,
},
{
address: withoutName,
coin_balance: '0',
tx_count: 0,
},
],
next_page_params: {
items_count: 50,
world: '0x18f01f12ca21b6fc97b917c3e32f671f8a933caa',
},
};
......@@ -230,7 +230,7 @@ export type AddressMudRecords = {
}
export type AddressMudRecordsItem = {
decoded: Record<string, string>;
decoded: Record<string, string | Array<string>>;
id: string;
is_deleted: boolean;
timestamp: string;
......
import { Box, useColorModeValue, chakra, HStack } from '@chakra-ui/react';
import { Box, useColorModeValue, chakra, Grid } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
......@@ -8,6 +8,8 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import useAddressQuery from '../utils/useAddressQuery';
type TableViewProps = {
scrollRef?: React.RefObject<HTMLDivElement>;
className?: string;
......@@ -40,7 +42,7 @@ const BreadcrumbItem = ({ text, href, isLast, scrollRef }: BreadcrumbItemProps)
if (isLast) {
return (
<HStack gap={ 2 } overflow="hidden">
<Grid gap={ 2 } overflow="hidden" templateColumns="auto 24px">
<Box
overflow="hidden"
whiteSpace="nowrap"
......@@ -49,12 +51,12 @@ const BreadcrumbItem = ({ text, href, isLast, scrollRef }: BreadcrumbItemProps)
{ text }
</Box>
<CopyToClipboard text={ href } type="link" mx={ 0 } color="text_secondary"/>
</HStack>
</Grid>
);
}
return (
<HStack gap={ 2 } overflow="hidden">
<Grid gap={ 2 } overflow="hidden" templateColumns="auto 24px">
<LinkInternal
href={ href }
onClick={ onLinkClick }
......@@ -65,7 +67,7 @@ const BreadcrumbItem = ({ text, href, isLast, scrollRef }: BreadcrumbItemProps)
{ text }
</LinkInternal>
{ !isLast && <IconSvg name="arrows/east" boxSize={ 6 } color={ iconColor }/> }
</HStack>
</Grid>
);
};
......@@ -73,6 +75,8 @@ const AddressMudBreadcrumbs = (props: TableViewProps | RecordViewProps) => {
const queryParams = { tab: 'mud', hash: props.hash };
const isMobile = useIsMobile();
const addressQuery = useAddressQuery({ hash: props.hash });
return (
<Box
display={ isMobile ? 'flex' : 'grid' }
......@@ -82,8 +86,9 @@ const AddressMudBreadcrumbs = (props: TableViewProps | RecordViewProps) => {
alignItems="center"
className={ props.className }
width="fit-content"
fontSize="sm"
>
<IconSvg name="MUD" boxSize={ 5 } color="green.500"/>
<IconSvg name="MUD" boxSize={ 5 } color={ addressQuery.data?.is_verified ? 'green.500' : 'text_secondary' }/>
<BreadcrumbItem
text="MUD World"
href={ route({ pathname: '/address/[hash]', query: queryParams }) }
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { mudRecord } from 'mocks/mud/mudTables';
import { test, expect, devices } from 'playwright/lib';
import AddressMudRecord from './AddressMudRecord';
const ADDRESS_HASH = 'hash';
const TABLE_ID = '123';
const RECORD_ID = '234';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_record', mudRecord, { pathParams: { hash: ADDRESS_HASH, table_id: TABLE_ID, record_id: RECORD_ID } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudRecord tableId={ TABLE_ID } recordId={ RECORD_ID }/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_record', mudRecord, { pathParams: { hash: ADDRESS_HASH, table_id: TABLE_ID, record_id: RECORD_ID } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudRecord tableId={ TABLE_ID } recordId={ RECORD_ID }/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
......@@ -10,6 +10,7 @@ import TruncatedValue from 'ui/shared/TruncatedValue';
import AddressMudBreadcrumbs from './AddressMudBreadcrumbs';
import AddressMudRecordValues from './AddressMudRecordValues';
import { getValueString } from './utils';
type Props ={
scrollRef?: React.RefObject<HTMLDivElement>;
......@@ -55,12 +56,12 @@ const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef
<Table borderRadius="8px" style={{ tableLayout: 'auto' }} width="100%" overflow="hidden">
{ data?.schema.key_names.length && data?.schema.key_names.map((keyName, index) => (
<Tr key={ keyName } borderBottomStyle={ index === data.schema.key_names.length - 1 ? 'hidden' : 'solid' }>
<Td fontWeight={ 600 } whiteSpace="nowrap">
<Td fontWeight={ 600 } whiteSpace="nowrap" fontSize="sm">
{ keyName } ({ data.schema.key_types[index] })
</Td>
<Td colSpan={ 2 }>
<Td colSpan={ 2 } fontSize="sm">
<Flex justifyContent="space-between">
<TruncatedValue value={ data.record.decoded[keyName] } mr={ 2 }/>
<TruncatedValue value={ getValueString(data.record.decoded[keyName]) } mr={ 2 }/>
{ index === 0 && <Box color="text_secondary">{ dayjs(data.record.timestamp).format('lll') }</Box> }
</Flex>
</Td>
......@@ -72,12 +73,12 @@ const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef
<Hide above="lg" ssr={ false }>
<>
{ data?.schema.key_names.length && data?.schema.key_names.map((keyName, index) => (
<VStack gap={ 1 } key={ keyName } alignItems="start">
<VStack gap={ 1 } key={ keyName } alignItems="start" fontSize="sm">
<Divider/>
<Text fontWeight={ 600 } whiteSpace="nowrap">
{ keyName } ({ data.schema.key_types[index] })
</Text>
<Text wordBreak="break-all">{ data.record.decoded[keyName] }</Text>
<Text wordBreak="break-word">{ getValueString(data.record.decoded[keyName]) }</Text>
{ index === 0 && <Box color="text_secondary">{ dayjs(data.record.timestamp).format('lll') }</Box> }
</VStack>
)) }
......
......@@ -3,6 +3,8 @@ import React from 'react';
import type { AddressMudRecord } from 'types/api/address';
import { getValueString } from './utils';
type Props ={
data?: AddressMudRecord;
}
......@@ -16,19 +18,19 @@ const AddressMudRecordValues = ({ data }: Props) => {
return (
<>
<Tr backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
<Td fontWeight={ 600 }>Field</Td>
<Td fontWeight={ 600 }>Type</Td>
<Td fontWeight={ 600 } w="100%" wordBreak="break-all">Value</Td>
<Tr backgroundColor={ valuesBgColor } borderBottomStyle="hidden" >
<Td fontWeight={ 600 } w="100px" fontSize="sm">Field</Td>
<Td fontWeight={ 600 } w="90px" fontSize="sm">Type</Td>
<Td fontWeight={ 600 } fontSize="sm">Value</Td>
</Tr>
{
data?.schema.value_names.map((valName, index) => (
<Tr key={ valName } backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
<Td whiteSpace="nowrap" py={ 0 } pb={ 4 }>{ valName }</Td>
<Td py={ 0 } pb={ 4 }>{ data.schema.value_types[index] }</Td>
<Td w="100%" wordBreak="break-all" py={ 0 } pb={ 4 }>
<Td fontSize="sm" w="100px" py={ 0 } pb={ 4 } pr={ 0 }wordBreak="break-all">{ valName }</Td>
<Td fontSize="sm" w="90px" py={ 0 } pb={ 4 } wordBreak="break-all">{ data.schema.value_types[index] }</Td>
<Td fontSize="sm" wordBreak="break-word" py={ 0 } pb={ 4 }>
<Box>
{ data.record.decoded[valName] }
{ getValueString(data.record.decoded[valName]) }
</Box>
</Td>
</Tr>
......
import type { StyleProps } from '@chakra-ui/react';
import { Box, Link, Table, Tbody, Td, Th, Tr, Flex, useColorModeValue, useBoolean } from '@chakra-ui/react';
import { Box, Link, Table, Tbody, Td, Th, Tr, Flex, useColorModeValue, useBoolean, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressMudRecords, AddressMudRecordsFilter, AddressMudRecordsSorting } from 'types/api/address';
import { route } from 'nextjs-routes';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressMudRecordsKeyFilter from './AddressMudRecordsKeyFilter';
import { getNameTypeText } from './utils';
import { getNameTypeText, getValueString } from './utils';
const COL_MIN_WIDTH = 180;
const COL_MIN_WIDTH_MOBILE = 140;
......@@ -28,6 +31,7 @@ type Props = {
filters: AddressMudRecordsFilter;
toggleTableHasHorisontalScroll: () => void;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
}
const AddressMudRecordsTable = ({
......@@ -39,6 +43,7 @@ const AddressMudRecordsTable = ({
setFilters,
toggleTableHasHorisontalScroll,
scrollRef,
hash,
}: Props) => {
const totalColsCut = data.schema.key_names.length + data.schema.value_names.length;
const isMobile = useIsMobile(false);
......@@ -46,23 +51,32 @@ const AddressMudRecordsTable = ({
const [ isOpened, setIsOpened ] = useBoolean(false);
const [ hasCut, setHasCut ] = useBoolean(isMobile ? totalColsCut > MIN_CUT_COUNT : true);
const containerRef = React.useRef<HTMLTableElement>(null);
const tableRef = React.useRef<HTMLTableElement>(null);
const router = useRouter();
const toggleIsOpen = React.useCallback(() => {
isOpened && tableRef.current?.scroll({ left: 0 });
setIsOpened.toggle();
toggleTableHasHorisontalScroll();
}, [ setIsOpened, toggleTableHasHorisontalScroll ]);
}, [ setIsOpened, toggleTableHasHorisontalScroll, isOpened ]);
const onRecordClick = React.useCallback((e: React.MouseEvent) => {
const newQuery = {
...router.query,
record_id: e.currentTarget.getAttribute('data-id') as string,
};
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
if (e.metaKey || e.ctrlKey) {
// Allow opening in a new tab/window with right-click or ctrl/cmd+click
return;
}
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: data.table.table_id, record_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef ]);
}, [ router, scrollRef, hash, data.table.table_id ]);
const handleFilterChange = React.useCallback((field: keyof AddressMudRecordsFilter) => (val: string) => {
setFilters(prev => {
......@@ -80,8 +94,8 @@ const AddressMudRecordsTable = ({
const keyBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
React.useEffect(() => {
if (hasCut && !colsCutCount && tableRef.current) {
const count = Math.floor((tableRef.current.getBoundingClientRect().width - CUT_COL_WIDTH) / COL_MIN_WIDTH);
if (hasCut && !colsCutCount && containerRef.current) {
const count = Math.floor((containerRef.current.getBoundingClientRect().width - CUT_COL_WIDTH) / COL_MIN_WIDTH);
if (totalColsCut > 2 && count - 1 < totalColsCut) {
setColsCutCount(count - 1);
} else {
......@@ -90,22 +104,15 @@ const AddressMudRecordsTable = ({
}
}, [ colsCutCount, data.schema, hasCut, setHasCut, totalColsCut ]);
const cutWidth = `${ CUT_COL_WIDTH }px `;
const colW = isMobile ? COL_MIN_WIDTH_MOBILE : COL_MIN_WIDTH;
const tdStyles: StyleProps = {
wordBreak: 'break-all',
whiteSpace: 'normal',
minW: `${ colW }px`,
w: `${ colW }px`,
};
const thStyles: StyleProps = {
wordBreak: 'break-word',
whiteSpace: 'normal',
minW: `${ colW }px`,
w: `${ colW }px`,
verticalAlign: 'baseline',
lineHeight: '20px',
};
const keys = (isOpened || !hasCut) ? data.schema.key_names : data.schema.key_names.slice(0, colsCutCount);
......@@ -113,16 +120,28 @@ const AddressMudRecordsTable = ({
const hasHorizontalScroll = isMobile || isOpened;
if (hasCut && !colsCutCount) {
return <Box w="100%" ref={ containerRef }></Box>;
}
const cutButton = (
<Th width={ `${ CUT_COL_WIDTH }px ` } verticalAlign="baseline">
<Tooltip label={ isOpened ? 'Hide columns' : 'Show all columns' }>
<Link onClick={ toggleIsOpen } aria-label="show/hide columns">...</Link>
</Tooltip>
</Th>
);
return (
// can't implement both horisontal table scroll and sticky header
<Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap">
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }} ref={ tableRef }>
<Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap" ref={ tableRef }>
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }}>
<Thead top={ hasHorizontalScroll ? 0 : top } display={ hasHorizontalScroll ? 'table' : 'table-header-group' } w="100%">
<Tr >
{ keys.map((keyName, index) => {
const text = getNameTypeText(keyName, data.schema.key_types[index]);
return (
<Th key={ keyName } { ...thStyles }>
<Th key={ keyName } { ...tdStyles }>
{ index < 2 ? (
<Flex>
<Link onClick={ onKeySortClick } data-id={ index } display="flex" alignItems="start" lineHeight="20px" mr={ 2 }>
......@@ -143,13 +162,13 @@ const AddressMudRecordsTable = ({
);
}) }
{ values.map((valName, index) => (
<Th key={ valName } { ...thStyles }>
<Th key={ valName } { ...tdStyles }>
{ capitalizeFirstLetter(valName) } ({ data.schema.value_types[index] })
</Th>
)) }
{ hasCut && !isOpened && <Th width={ cutWidth }><Link onClick={ toggleIsOpen }>...</Link></Th> }
<Th { ...thStyles }>Modified</Th>
{ hasCut && isOpened && <Th width={ cutWidth }><Link onClick={ toggleIsOpen }>...</Link></Th> }
{ hasCut && !isOpened && cutButton }
<Th { ...tdStyles }>Modified</Th>
{ hasCut && isOpened && cutButton }
</Tr>
</Thead>
<Tbody display={ hasHorizontalScroll ? 'table' : 'table-row-group' } w="100%">
......@@ -157,17 +176,23 @@ const AddressMudRecordsTable = ({
<Tr key={ item.id }>
{ keys.map((keyName, index) => (
<Td key={ keyName } backgroundColor={ keyBgColor } { ...tdStyles }>
{ index === 0 ?
<Link onClick={ onRecordClick } data-id={ item.id } fontWeight={ 700 }>{ item.decoded[keyName].toString() }</Link> :
item.decoded[keyName].toString()
}
{ index === 0 ? (
<LinkInternal
onClick={ onRecordClick }
data-id={ item.id }
fontWeight={ 700 }
href={ route({ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: data.table.table_id, record_id: item.id } }) }
>
{ getValueString(item.decoded[keyName]) }
</LinkInternal>
) : getValueString(item.decoded[keyName]) }
</Td>
)) }
{ values.map((valName) =>
<Td key={ valName } { ...tdStyles }>{ item.decoded[valName].toString() }</Td>) }
{ hasCut && !isOpened && <Td width={ cutWidth }></Td> }
<Td key={ valName } { ...tdStyles }>{ getValueString(item.decoded[valName]) }</Td>) }
{ hasCut && !isOpened && <Td width={ `${ CUT_COL_WIDTH }px ` }></Td> }
<Td { ...tdStyles } color="text_secondary">{ dayjs(item.timestamp).format('lll') }</Td>
{ hasCut && isOpened && <Td width={ cutWidth }></Td> }
{ hasCut && isOpened && <Td width={ `${ CUT_COL_WIDTH }px ` }></Td> }
</Tr>
)) }
</Tbody>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { mudRecords } from 'mocks/mud/mudTables';
import { test, expect } from 'playwright/lib';
import AddressMudTable from './AddressMudTable';
const ADDRESS_HASH = 'hash';
const TABLE_ID = '123';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_records', mudRecords, { pathParams: { hash: ADDRESS_HASH, table_id: TABLE_ID } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudTable tableId={ TABLE_ID }/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('expanded view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_records', mudRecords, { pathParams: { hash: ADDRESS_HASH, table_id: TABLE_ID } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudTable tableId={ TABLE_ID }/>
</Box>,
{ hooksConfig },
);
await component.locator('a[aria-label="show/hide columns"]').first().click();
await expect(component).toHaveScreenshot();
});
......@@ -125,6 +125,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
filters={ filters }
toggleTableHasHorisontalScroll={ setTableHasHorisontalScroll.toggle }
scrollRef={ scrollRef }
hash={ hash }
/>
) : null;
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { mudTables } from 'mocks/mud/mudTables';
import { test, expect } from 'playwright/lib';
import AddressMudTables from './AddressMudTables';
const ADDRESS_HASH = 'hash';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, q: 'o' },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_tables', mudTables, { pathParams: { hash: ADDRESS_HASH }, queryParams: { q: 'o' } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudTables/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with schema opened +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_mud_tables', mudTables, { pathParams: { hash: ADDRESS_HASH }, queryParams: { q: 'o' } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressMudTables/>
</Box>,
{ hooksConfig },
);
await component.locator('div[aria-label="View schema"]').first().click();
await expect(component).toHaveScreenshot();
});
......@@ -2,6 +2,7 @@ import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useDebounce from 'lib/hooks/useDebounce';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -27,11 +28,12 @@ const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
const hash = getQueryParamString(router.query.hash);
const q = getQueryParamString(router.query.q);
const [ searchTerm, setSearchTerm ] = React.useState<string>(q || '');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'address_mud_tables',
pathParams: { hash },
filters: { q: searchTerm },
filters: { q: debouncedSearchTerm },
scrollRef,
options: {
enabled: isQueryEnabled,
......@@ -71,6 +73,7 @@ const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
isLoading={ isPlaceholderData }
top={ ACTION_BAR_HEIGHT_DESKTOP }
scrollRef={ scrollRef }
hash={ hash }
/>
</Hide>
<Show below="lg" ssr={ false }>
......@@ -79,6 +82,7 @@ const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
key={ item.table.table_id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
hash={ hash }
/>
)) }
</Show>
......
......@@ -4,30 +4,41 @@ import React from 'react';
import type { AddressMudTableItem } from 'types/api/address';
import { route } from 'nextjs-routes';
import Tag from 'ui/shared/chakra/Tag';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = {
item: AddressMudTableItem;
isLoading: boolean;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
};
const AddressMudTablesListItem = ({ item, isLoading, scrollRef }: Props) => {
const AddressMudTablesListItem = ({ item, isLoading, scrollRef, hash }: Props) => {
const [ isOpened, setIsOpened ] = useBoolean(false);
const router = useRouter();
const onTableClick = React.useCallback((e: React.MouseEvent) => {
const newQuery = {
...router.query,
table_id: e.currentTarget.getAttribute('data-id') as string,
};
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
if (e.metaKey || e.ctrlKey) {
// Allow opening in a new tab/window with right-click or ctrl/cmd+click
return;
}
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef ]);
}, [ router, scrollRef, hash ]);
return (
<ListItemMobile rowGap={ 3 } fontSize="sm" py={ 3 }>
......@@ -41,15 +52,21 @@ const AddressMudTablesListItem = ({ item, isLoading, scrollRef }: Props) => {
cursor="pointer"
onClick={ setIsOpened.toggle }
transitionDuration="faster"
aria-label="View schema"
/>
</Link>
</Skeleton>
<Box flexGrow="1">
<Flex justifyContent="space-between" height={ 6 } alignItems="center" mb={ 3 }>
<Skeleton isLoaded={ !isLoading }>
<Link onClick={ onTableClick } data-id={ item.table.table_id } fontWeight={ 500 }>
<LinkInternal
onClick={ onTableClick }
data-id={ item.table.table_id }
fontWeight={ 500 }
href={ route({ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: item.table.table_id } }) }
>
{ item.table.table_full_name }
</Link>
</LinkInternal>
</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
{ item.table.table_type }
......
......@@ -12,10 +12,11 @@ type Props = {
isLoading: boolean;
top: number;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
}
//sorry for the naming
const AddressMudTablesTable = ({ items, isLoading, top, scrollRef }: Props) => {
const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
......@@ -33,6 +34,7 @@ const AddressMudTablesTable = ({ items, isLoading, top, scrollRef }: Props) => {
item={ item }
isLoading={ isLoading }
scrollRef={ scrollRef }
hash={ hash }
/>
)) }
</Tbody>
......
......@@ -4,28 +4,39 @@ import React from 'react';
import type { AddressMudTableItem } from 'types/api/address';
import { route } from 'nextjs-routes';
import Tag from 'ui/shared/chakra/Tag';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
type Props = {
item: AddressMudTableItem;
isLoading: boolean;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
};
const AddressMudTablesTableItem = ({ item, isLoading, scrollRef }: Props) => {
const AddressMudTablesTableItem = ({ item, isLoading, scrollRef, hash }: Props) => {
const [ isOpened, setIsOpened ] = useBoolean(false);
const router = useRouter();
const onTableClick = React.useCallback((e: React.MouseEvent) => {
const newQuery = {
...router.query,
table_id: e.currentTarget.getAttribute('data-id') as string,
};
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
if (e.metaKey || e.ctrlKey) {
// Allow opening in a new tab/window with right-click or ctrl/cmd+click
return;
}
e.preventDefault();
router.push(
{ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: e.currentTarget.getAttribute('data-id') as string } },
undefined,
{ shallow: true },
);
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef ]);
}, [ router, scrollRef, hash ]);
return (
<>
......@@ -40,15 +51,21 @@ const AddressMudTablesTableItem = ({ item, isLoading, scrollRef }: Props) => {
cursor="pointer"
onClick={ setIsOpened.toggle }
transitionDuration="faster"
aria-label="View schema"
/>
</Link>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<Link onClick={ onTableClick } data-id={ item.table.table_id }>
<LinkInternal
href={ route({ pathname: '/address/[hash]', query: { hash, tab: 'mud', table_id: item.table.table_id } }) }
data-id={ item.table.table_id }
onClick={ onTableClick }
fontWeight={ 700 }
>
{ item.table.table_full_name }
</Link>
</LinkInternal>
</Skeleton>
</Td>
<Td verticalAlign="middle">
......@@ -69,7 +86,7 @@ const AddressMudTablesTableItem = ({ item, isLoading, scrollRef }: Props) => {
<Table>
{ Boolean(item.schema.key_names.length) && (
<Tr>
<Td width="80px" fontSize="sm" fontWeight={ 600 } py={ 2 }>Key</Td>
<Td width="80px" fontSize="sm" fontWeight={ 600 } py={ 2 } verticalAlign="middle">Key</Td>
<Td py={ 2 }>
<VStack gap={ 1 } alignItems="start">
{ item.schema.key_names.map((name, index) => (
......
......@@ -8,3 +8,11 @@ export const SORT_SEQUENCE: Record<'key0' | 'key1', Array<'desc' | 'asc' | undef
export const getNameTypeText = (name: string, type: string) => {
return capitalizeFirstLetter(name) + ' (' + type + ')';
};
export const getValueString = (value: string | Array<string>) => {
if (Array.isArray(value)) {
return value.join(', ');
}
return value;
};
......@@ -7,15 +7,9 @@ import type { MudWorldItem } from 'types/api/mudWorlds';
import config from 'configs/app';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
const mudFrameworkFeature = config.features.mudFramework;
type Props = { item: MudWorldItem; isLoading?: boolean };
const MudWorldsTableItem = ({ item, isLoading }: Props) => {
if (!mudFrameworkFeature.isEnabled) {
return null;
}
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');
......
......@@ -272,16 +272,21 @@ const AddressPageContent = () => {
<RoutedTabs tabs={ tabs } tabListProps={{ mt: 6 }} isLoading={ isTabsLoading }/>;
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
if (!hasGoBackLink) {
return;
if (appProps.referrer && appProps.referrer.includes('/accounts')) {
return {
label: 'Back to top accounts list',
url: appProps.referrer,
};
}
if (appProps.referrer && appProps.referrer.includes('/mud-worlds')) {
return {
label: 'Back to top accounts list',
label: 'Back to MUD worlds list',
url: appProps.referrer,
};
}
return;
}, [ appProps.referrer ]);
const titleSecondRow = (
......
import React from 'react';
import { mudWorlds } from 'mocks/mud/mudWorlds';
import { test, expect } from 'playwright/lib';
import MudWorlds from './MudWorlds';
test('default view +@mobile', async({ mockTextAd, mockApiResponse, render }) => {
await mockTextAd();
await mockApiResponse('mud_worlds', mudWorlds);
const component = await render(<MudWorlds/>);
await expect(component).toHaveScreenshot();
});
......@@ -40,7 +40,7 @@ const TableColumnFilterWrapper = ({ columnName, isActive, className, children, i
variant="ghost"
w="20px"
h="20px"
icon={ <IconSvg name="filter" w="20px" h="20px"/> }
icon={ <IconSvg name="filter" w="19px" h="19px"/> }
isActive={ isActive }
isDisabled={ isLoading }
borderRadius="4px"
......
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