Commit 88c9e4b5 authored by tom's avatar tom

more test fixes

parent 4c868b50
import React from 'react';
import getComponentDisplayName from '../utils/getComponentDisplayName';
import { Button, type ButtonProps } from './button';
export interface IconButtonProps extends Omit<ButtonProps, 'size'> {
......@@ -8,7 +9,14 @@ export interface IconButtonProps extends Omit<ButtonProps, 'size'> {
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
function IconButton(props, ref) {
const { size, variant = 'plain', ...rest } = props;
const { size, variant = 'plain', children, ...rest } = props;
// FIXME: I have to clone the children instead of using _icon props because of style overrides
// in some pw tests for some reason the _icon style will be applied before the style of child (IconSvg component)
const child = React.Children.only<React.ReactElement>(children as React.ReactElement);
const clonedChildren = size && getComponentDisplayName(child.type) === 'IconSvg' ?
React.cloneElement(child, { boxSize: 5 }) :
child;
const sizeStyle = (() => {
switch (size) {
......@@ -38,11 +46,13 @@ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
alignItems="center"
p={ 0 }
minW="auto"
{ ...sizeStyle }
flexShrink="0"
variant={ variant }
{ ...sizeStyle }
{ ...rest }
/>
>
{ clonedChildren }
</Button>
);
},
);
......@@ -9,11 +9,6 @@ export const recipe = defineRecipe({
_disabled: {
opacity: 'control.disabled',
},
// FIXME have to override the Chakra UI styles for the SVG icon inside the button
// try to find a better solution
'& svg': {
boxSize: 'auto',
},
_loading: {
bgColor: 'unset',
},
......
......@@ -154,7 +154,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
</DetailedInfo.ItemValue>
</>
) }
{ data.is_contract && data.implementations && data.implementations?.length > 0 && (
{ !addressQuery.isPlaceholderData && data.is_contract && data.implementations && data.implementations?.length > 0 && (
<AddressImplementations
data={ data.implementations }
isLoading={ addressQuery.isPlaceholderData }
......
......@@ -144,7 +144,7 @@ test.describe('mobile', () => {
{ hooksConfig },
);
await component.getByLabel('list').click();
await component.locator('button').filter({ hasText: 'List' }).click();
await expect(component).toHaveScreenshot();
});
......
......@@ -41,7 +41,7 @@ test.describe('base view', () => {
);
});
test('+@mobile', async() => {
test('desktop', async() => {
await expect(component).toHaveScreenshot();
});
......@@ -55,6 +55,31 @@ test.describe('base view', () => {
});
});
test.describe('base view', () => {
test.use({ viewport: pwConfig.viewport.mobile });
test('mobile', async({ render, mockApiResponse }) => {
await mockApiResponse(
'address_txs',
{
items: [
txMock.base,
{ ...txMock.base, hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194' },
],
next_page_params: DEFAULT_PAGINATION,
},
{ pathParams: { hash: CURRENT_ADDRESS } },
);
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
test.describe('socket', () => {
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
......
......@@ -64,7 +64,8 @@ const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect,
placeholder="Select contract"
defaultValue={ [ selectedItem.address ] }
onValueChange={ handleItemSelect }
maxW={{ base: '180px', lg: 'none' }}
maxW={{ base: '180px', lg: '400px' }}
w="fit-content"
loading={ isLoading }
/>
<Flex alignItems="center">
......
......@@ -18,14 +18,7 @@ const ContractDetailsInfoItem = ({ label, children, className, isLoading, hint }
<Skeleton loading={ isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>
<Flex alignItems="center">
{ label }
{ hint && (
<Hint
label={ hint }
ml={ 2 }
color={{ _light: 'gray.600', _dark: 'gray.400' }}
tooltipProps={{ positioning: { placement: 'bottom' } }}
/>
) }
{ hint && <Hint label={ hint } ml={ 2 }/> }
</Flex>
</Skeleton>
<Skeleton loading={ isLoading }>{ children }</Skeleton>
......
......@@ -218,7 +218,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
</Flex>
) }
<Flex rowGap={ 5 } flexDir="column">
<div>
<Flex flexDir="column">
<CopyToClipboard text={ signingMessage } ml="auto"/>
<FormFieldText<Fields>
name="message"
......@@ -231,7 +231,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
minH: 'auto',
}}
/>
</div>
</Flex>
{ !noWeb3Provider && (
<RadioGroup
onValueChange={ handleSignMethodChange }
......
......@@ -7,23 +7,23 @@ import imageBlobWithZeroesBytes from './image_with_zeroes.blob';
test.use({ viewport: { width: 500, height: 300 } });
test('text', async({ render }) => {
test('text', async({ render, page }) => {
// eslint-disable-next-line max-len
const data = '0xE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A280E2A3A4E2A1B6E2A0BFE2A0BFE2A0B7E2A3B6E2A384E2A080E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B0E2A1BFE2A081E2A080E2A080E2A280E2A380E2A180E2A099E2A3B7E2A180E2A080E2A080E2A0800AE2A080E2A080E2A080E2A180E2A080E2A080E2A080E2A080E2A080E2A2A0E2A3BFE2A081E2A080E2A080E2A080E2A098E2A0BFE2A083E2A080E2A2B8E2A3BFE2A3BFE2A3BFE2A3BF0AE2A080E2A3A0E2A1BFE2A09BE2A2B7E2A3A6E2A180E2A080E2A080E2A088E2A3BFE2A184E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B8E2A3BFE2A3BFE2A3BFE2A09F0AE2A2B0E2A1BFE2A081E2A080E2A080E2A099E2A2BFE2A3A6E2A3A4E2A3A4E2A3BCE2A3BFE2A384E2A080E2A080E2A080E2A080E2A080E2A2B4E2A19FE2A09BE2A08BE2A081E2A0800AE2A3BFE2A087E2A080E2A080E2A080E2A080E2A080E2A089E2A089E2A089E2A089E2A089E2A081E2A080E2A080E2A080E2A080E2A080E2A088E2A3BFE2A180E2A080E2A080E2A0800AE2A3BFE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2B9E2A187E2A080E2A080E2A0800AE2A3BFE2A186E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3BCE2A187E2A080E2A080E2A0800AE2A0B8E2A3B7E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2A0E2A1BFE2A080E2A080E2A080E2A0800AE2A080E2A0B9E2A3B7E2A3A4E2A380E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A380E2A3B0E2A1BFE2A081E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A089E2A099E2A09BE2A0BFE2A0B6E2A3B6E2A3B6E2A3B6E2A3B6E2A3B6E2A0B6E2A0BFE2A09FE2A09BE2A089E2A080E2A080E2A080E2A080E2A080E2A080';
const component = await render(<BlobData hash="0x01" data={ data }/>);
await expect(component).toHaveScreenshot();
await component.getByRole('button', { name: 'Raw' }).click();
await component.getByText('UTF-8').click();
await component.getByRole('combobox').click();
await page.getByRole('option', { name: /utf/i }).click();
await expect(component).toHaveScreenshot();
});
test('image', async({ render }) => {
test('image', async({ render, page }) => {
// eslint-disable-next-line max-len
const data = '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403000000C8D2C4410000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000027504C54454C69712B6CB02A6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB0F4205A540000000C74524E5300ED2F788CD91B99475C09B969CFA99D0000004F7A5458745261772070726F66696C65207479706520697074630000789CE3CA2C2849E6520003230B2E630B1323134B9314031320448034C3640323B35420CBD8D4C8C4CCC41CC407CB8048A04A2E0028950EE32A226D1F0000000970485973000084DF000084DF0195C81C33000000F24944415438CB636000018E983367CE482780D90CDA40F6991D0C4820152472A60ACCE6DA03629F4E40929E03961602B39964C09C0624691B24690E88F48461215D03160903B3D962C01C07842C2758C341A80643B0B40484C3646C6C5C78E6E016171723A8E215262EEE31670E161B1B7731304C05AB155EC08002C0D172E6F80206884DBB50651938CF4003FE0CBA4390E3C56064482F53525252C329CD562A2828283A0197340B22AAB0494332C311FCD2C747A547A58996C69998D8F12745B68DA0846C85331B2CEAE8E8681A81D91F8B348C4605D0527B02A4283FA88026CD05163EAAC0900ED21EC9800EC0C2110C002BBA9FE999B920330000000049454E44AE426082';
const component = await render(<BlobData hash="0x01" data={ data }/>);
await expect(component).toHaveScreenshot();
await component.getByRole('button', { name: 'Image' }).click();
await component.getByText('Base64').click();
await component.getByRole('combobox').click();
await page.getByRole('option', { name: /base64/i }).click();
await expect(component).toHaveScreenshot();
});
......
......@@ -33,7 +33,7 @@ interface Props {
}
const BlobData = ({ data, isLoading, hash }: Props) => {
const [ format, setFormat ] = React.useState<Format>('Raw');
const [ format, setFormat ] = React.useState<Array<Format>>([ 'Raw' ]);
const guessedType = React.useMemo(() => {
if (isLoading) {
......@@ -52,17 +52,17 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
React.useEffect(() => {
if (isImage) {
setFormat('Image');
setFormat([ 'Image' ]);
}
}, [ isImage ]);
const handleFormatChange = React.useCallback(({ value }: { value: Array<string> }) => {
setFormat(value[0] as Format);
setFormat(value as Array<Format>);
}, []);
const handleDownloadButtonClick = React.useCallback(() => {
const fileBlob = (() => {
switch (format) {
switch (format[0]) {
case 'Image': {
const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes);
......@@ -85,7 +85,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
}, [ data, format, guessedType, hash ]);
const content = (() => {
switch (format) {
switch (format[0]) {
case 'Image': {
if (!guessedType?.mime?.startsWith('image/')) {
return <RawDataSnippet data="Not an image" showCopy={ false } isLoading={ isLoading }/>;
......@@ -119,7 +119,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
<Select
collection={ collection }
placeholder="Select type"
defaultValue={ [ format ] }
value={ format }
onValueChange={ handleFormatChange }
ml={ 5 }
w="100px"
......
......@@ -103,19 +103,19 @@ const LatestBlocks = () => {
return (
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
<Heading level="3" mb={ 3 }>Latest blocks</Heading>
<Heading level="3">Latest blocks</Heading>
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Skeleton loading={ statsQueryResult.isPlaceholderData } mt={ 1 } display="inline-block">
<Text as="span" fontSize="sm">
<Skeleton loading={ statsQueryResult.isPlaceholderData } mt={ 2 } display="inline-block" textStyle="sm">
<Text as="span">
Network utilization:{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
<Text as="span" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text>
</Skeleton>
) }
{ statsQueryResult.data?.celo && (
<Box whiteSpace="pre-wrap" fontSize="sm">
<Box whiteSpace="pre-wrap" textStyle="sm" mt={ 2 }>
<span>Current epoch: </span>
<chakra.span fontWeight={ 700 }>#{ statsQueryResult.data.celo.epoch_number }</chakra.span>
</Box>
......
......@@ -54,7 +54,7 @@ const LatestBlocksItem = ({ block, isLoading, animation }: Props) => {
ml={ 2 }
/>
</Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" textStyle="sm">
<Skeleton loading={ isLoading }>Txn</Skeleton>
<Skeleton loading={ isLoading } color="text.secondary"><span>{ block.transaction_count }</span></Skeleton>
......
......@@ -44,7 +44,7 @@ const InteropMessagesTableItem = ({ item, isLoading }: Props) => {
</TableCell>
<TableCell>
{ item.init_chain !== undefined ?
<AddressEntityInterop address={{ hash: item.target }} isLoading={ isLoading } truncation="constant" chain={ item.init_chain }/> :
<AddressEntityInterop address={{ hash: item.target }} isLoading={ isLoading } truncation="constant" chain={ item.init_chain } w="min-content"/> :
<AddressEntity address={{ hash: item.target }} isLoading={ isLoading } truncation="constant"/>
}
</TableCell>
......@@ -56,7 +56,7 @@ const InteropMessagesTableItem = ({ item, isLoading }: Props) => {
</TableCell>
<TableCell>
{ item.relay_chain !== undefined ?
<AddressEntityInterop address={{ hash: item.target }} isLoading={ isLoading } truncation="constant" chain={ item.relay_chain }/> :
<AddressEntityInterop address={{ hash: item.target }} isLoading={ isLoading } truncation="constant" chain={ item.relay_chain } w="min-content"/> :
<AddressEntity address={{ hash: item.target }} isLoading={ isLoading } truncation="constant"/>
}
</TableCell>
......
......@@ -2,26 +2,41 @@ import React from 'react';
import * as interopMessageMock from 'mocks/interop/interop';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import InteropMessages from './InteropMessages';
test('default view +@mobile', async({ render, mockTextAd, mockAssetResponse, mockApiResponse }) => {
const MESSAGES_RESPONSE = {
items: [
interopMessageMock.interopMessageIn,
interopMessageMock.interopMessageIn1,
interopMessageMock.interopMessageOut,
interopMessageMock.interopMessageOut1,
].map((item, index) => ({ ...item, init_transaction_hash: `${ item.init_transaction_hash.slice(0, -1) }${ index }` })),
next_page_params: {
init_transaction_hash: '1',
items_count: 4,
timestamp: 1719456000,
},
};
test('default view', async({ render, mockTextAd, mockAssetResponse, mockApiResponse }) => {
await mockTextAd();
await mockAssetResponse(interopMessageMock.chain.chain_logo as string, './playwright/mocks/image_s.jpg');
await mockApiResponse('optimistic_l2_interop_messages', {
items: [
interopMessageMock.interopMessageIn,
interopMessageMock.interopMessageIn1,
interopMessageMock.interopMessageOut,
interopMessageMock.interopMessageOut1,
],
next_page_params: {
init_transaction_hash: '1',
items_count: 4,
timestamp: 1719456000,
},
});
await mockApiResponse('optimistic_l2_interop_messages', MESSAGES_RESPONSE);
await mockApiResponse('optimistic_l2_interop_messages_count', 4000000);
const component = await render(<InteropMessages/>);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: pwConfig.viewport.mobile });
test('default view', async({ render, mockTextAd, mockAssetResponse, mockApiResponse }) => {
await mockTextAd();
await mockAssetResponse(interopMessageMock.chain.chain_logo as string, './playwright/mocks/image_s.jpg');
await mockApiResponse('optimistic_l2_interop_messages', MESSAGES_RESPONSE);
await mockApiResponse('optimistic_l2_interop_messages_count', 4000000);
const component = await render(<InteropMessages/>);
await expect(component).toHaveScreenshot();
});
});
......@@ -63,7 +63,7 @@ const NameDomain = () => {
variant="subheading"
/>
{ infoQuery.data?.resolved_address && (
<Flex alignItems="center" maxW="100%" columnGap={ 3 }>
<Flex alignItems="center" maxW="100%" columnGap={ 2 }>
<AddressEntity
address={ infoQuery.data?.resolved_address }
isLoading={ isLoading }
......
......@@ -51,11 +51,10 @@ const RewardsDashboard = () => {
title="Dashboard"
secondRow={ (
<span>
The Blockscout Merits Program is just getting started! Learn more about the details,
features, and future plans in our{ ' ' }
<Link external href={ `https://merits.blockscout.com/?tab=users&utm_source=${ config.chain.id }&utm_medium=text-banner` }>
blog post
</Link>.
Explore the Merits Hub
</Link>{ ' ' }
to learn more info about a program, spend your Merits, learn how to earn more, and much more.
</span>
) }
/>
......
......@@ -41,7 +41,7 @@ const UserOpsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Value>
<Skeleton loading={ isLoading }>
{ externalLinks.map((link) => (
<Link external noIcon href={ link.url } key={ link.url } display="inline-flex">
<Link external href={ link.url } key={ link.url } display="inline-flex">
<Image src={ link.image } alt={ link.title } boxSize={ 5 } mr={ 2 }/>
{ link.title }
</Link>
......
......@@ -71,13 +71,13 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 6 } mt={ 8 } rowGap={ 3 }>
{ hasErrors && (
<Link href={ route({ pathname: '/public-tags/submit', query: startOverButtonQuery }) } asChild>
<Button variant="outline">
<Button variant="outline" w={{ base: '100%', lg: 'auto' }}>
Start over
</Button>
</Link>
) }
<Link href={ route({ pathname: '/public-tags/submit' }) } asChild>
<Button>Add new tag</Button>
<Button w={{ base: '100%', lg: 'auto' }}>Add new tag</Button>
</Link>
</Flex>
</div>
......
......@@ -21,8 +21,8 @@ const ButtonItem = ({ className, label, onClick, icon, isDisabled }: Props) => {
className={ className }
onClick={ onClick }
disabled={ isDisabled }
size="md"
variant="icon_secondary"
boxSize={ 8 }
_icon={{ boxSize: 6 }}
>
{ typeof icon === 'string' ? <IconSvg name={ icon }/> : icon }
......
import type { FlexProps } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { Flex, useToken } from '@chakra-ui/react';
import React from 'react';
export interface Props extends FlexProps {
......@@ -10,6 +10,7 @@ export interface Props extends FlexProps {
const ContainerWithScrollY = ({ gradientHeight, children, onScrollVisibilityChange, ...rest }: Props) => {
const ref = React.useRef<HTMLDivElement>(null);
const [ hasScroll, setHasScroll ] = React.useState(false);
const gradientStopColor = useToken('colors', 'global.body.bg');
React.useEffect(() => {
if (!ref.current) {
......@@ -29,11 +30,11 @@ const ContainerWithScrollY = ({ gradientHeight, children, onScrollVisibilityChan
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
bottom: 1,
left: 0,
right: '20px',
height: `${ gradientHeight }px`,
bgGradient: { _light: `linear(to-b, transparent, {colors.white}`, _dark: `linear(to-b, transparent, {colors.black})` },
bg: `linear-gradient(to bottom, transparent, ${ gradientStopColor })`,
} : undefined }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? `${ gradientHeight }px` : 0 }
......
......@@ -15,14 +15,18 @@ interface Props extends HTMLChakraProps<'div'> {
isLoading?: boolean;
}
const IconSvg = ({ name, isLoading = false, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
return (
<Skeleton loading={ isLoading } display="inline-block" asChild { ...props } ref={ ref }>
<chakra.svg w="100%" h="100%">
<use href={ `${ href }#${ name }` }/>
</chakra.svg>
</Skeleton>
);
};
const IconSvg = React.forwardRef(
function IconSvg({ name, isLoading = false, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) {
return (
<Skeleton loading={ isLoading } display="inline-block" asChild { ...props } ref={ ref }>
<chakra.svg w="100%" h="100%">
<use href={ `${ href }#${ name }` }/>
</chakra.svg>
</Skeleton>
);
},
);
export default React.forwardRef(IconSvg);
IconSvg.displayName = 'IconSvg';
export default IconSvg;
......@@ -14,10 +14,9 @@ interface Props extends IconButtonProps {
const ButtonBackTo = ({ href, hint, ...rest }: Props) => {
const button = (
<IconButton { ...rest }>
<IconButton { ...rest } boxSize={ 6 }>
<IconSvg
name="arrows/east"
boxSize={ 6 }
transform="rotate(180deg)"
color="icon.backTo"
_hover={{ color: 'link.primary.hover' }}
......
......@@ -39,7 +39,7 @@ test('base view +@dark-mode', async({ render, page }) => {
});
await expect(component).toHaveScreenshot();
await component.locator('.chakra-menu__menu-button').click();
await component.getByLabel('Open chart options menu').click();
await expect(component).toHaveScreenshot();
await page.mouse.move(0, 0);
......
......@@ -74,7 +74,7 @@ const AddressEntryInterop = (props: Props) => {
);
return (
<AddressEntity.Container>
<AddressEntity.Container className={ props.className }>
{ props.chain && (
<Tooltip content={ `Address on ${ props.chain.chain_name ? props.chain.chain_name : 'external chain' } (chain id ${ props.chain.chain_id })` }>
{ addressIcon }
......
......@@ -22,7 +22,7 @@ type Props = {
const IconStub = ({ isLoading }: { isLoading?: boolean }) => {
return (
<Skeleton
loading={ !isLoading }
loading={ isLoading }
display="flex"
minWidth="20px"
h="20px"
......
......@@ -40,7 +40,7 @@ const GasInfoTooltip = ({ children, data, dataUpdatedAt, placement }: Props) =>
3 : 2;
const content = (
<Flex flexDir="column" textStyle="xs" rowGap={ 3 }>
<Flex flexDir="column" textStyle="xs" rowGap={ 3 } className="dark">
{ data.gas_price_updated_at && (
<Flex justifyContent="space-between" alignItems="center">
<Box color="text.secondary">Last update</Box>
......@@ -56,7 +56,7 @@ const GasInfoTooltip = ({ children, data, dataUpdatedAt, placement }: Props) =>
<GasInfoTooltipRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoTooltipRow name="Slow" info={ data.gas_prices.slow }/>
</Grid>
<Link href={ route({ pathname: '/gas-tracker' }) } className="dark">
<Link href={ route({ pathname: '/gas-tracker' }) }>
Gas tracker overview
</Link>
</Flex>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment