Commit 3ecc9e5a authored by tom goriunov's avatar tom goriunov Committed by GitHub

Case: One button in a title (private/public tag) (#1593)

Fixes #1582
parent 6b43f9bd
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import AccountActionsMenu from './AccountActionsMenu';
test.use({ viewport: { width: 200, height: 200 } });
test.describe('with multiple items', async() => {
const hooksConfig = {
router: {
query: { hash: '<hash>' },
pathname: '/token/[hash]',
isReady: true,
},
};
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').click();
await expect(page).toHaveScreenshot();
});
test('base view with styles', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').click();
await expect(page).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('loading with styles', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
test.describe('with one item', async() => {
const hooksConfig = {
router: {
query: { hash: '<hash>' },
pathname: '/tx/[hash]',
isReady: true,
},
};
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').hover();
await expect(page).toHaveScreenshot();
});
test('base view with styles', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').hover();
await expect(page).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
import { IconButton, Menu, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { Box, IconButton, Menu, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemProps } from './types';
import config from 'configs/app';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -33,30 +35,55 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
return null;
}
const items = [
{
render: (props: ItemProps) => <TokenInfoMenuItem { ...props }/>,
enabled: isTokenPage && config.features.addressVerification.isEnabled,
},
{
render: (props: ItemProps) => <PrivateTagMenuItem { ...props } entityType={ isTxPage ? 'tx' : 'address' }/>,
enabled: true,
},
{
render: (props: ItemProps) => <PublicTagMenuItem { ...props }/>,
enabled: !isTxPage,
},
].filter(({ enabled }) => enabled);
if (items.length === 0) {
return null;
}
if (isLoading) {
return <Skeleton w="36px" h="32px" borderRadius="base" className={ className }/>;
}
if (items.length === 1) {
return (
<Box className={ className }>
{ items[0].render({ type: 'button', hash, onBeforeClick: isAccountActionAllowed }) }
</Box>
);
}
return (
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base" className={ className }>
<MenuButton
as={ IconButton }
size="sm"
variant="outline"
colorScheme="gray"
px="7px"
onClick={ handleButtonClick }
icon={ <IconSvg name="dots" boxSize="18px"/> }
/>
</Skeleton>
<MenuButton
as={ IconButton }
className={ className }
size="sm"
variant="outline"
colorScheme="gray"
px="7px"
onClick={ handleButtonClick }
icon={ <IconSvg name="dots" boxSize="18px"/> }
/>
<MenuList minWidth="180px" zIndex="popover">
{ isTokenPage && config.features.addressVerification.isEnabled &&
<TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
<PrivateTagMenuItem
py={ 2 }
px={ 4 }
hash={ hash }
onBeforeClick={ isAccountActionAllowed }
type={ isTxPage ? 'tx' : 'address' }
/>
{ !isTxPage && <PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
{ items.map(({ render }, index) => (
<React.Fragment key={ index }>
{ render({ type: 'menu_item', hash, onBeforeClick: isAccountActionAllowed }) }
</React.Fragment>
)) }
</MenuList>
</Menu>
);
......
import { MenuItem, chakra, useDisclosure } from '@chakra-ui/react';
import { useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import type { Address } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
......@@ -12,19 +13,23 @@ import AddressModal from 'ui/privateTags/AddressModal/AddressModal';
import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: () => boolean;
type?: 'address' | 'tx';
entityType?: 'address' | 'tx';
type: ItemType;
}
const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }: Props) => {
const PrivateTagMenuItem = ({ className, hash, onBeforeClick, entityType = 'address', type }: Props) => {
const modal = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const queryKey = getResourceKey(type === 'tx' ? 'tx' : 'address', { pathParams: { hash } });
const queryKey = getResourceKey(entityType === 'tx' ? 'tx' : 'address', { pathParams: { hash } });
const queryData = queryClient.getQueryData<Address | Transaction>(queryKey);
const handleClick = React.useCallback(() => {
......@@ -58,13 +63,26 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }
pageType,
};
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add private tag" icon="privattags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
);
}
}
})();
return (
<>
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
{ type === 'tx' ?
{ element }
{ entityType === 'tx' ?
<TransactionModal { ...modalProps } data={{ transaction_hash: hash }}/> :
<AddressModal { ...modalProps } data={{ address_hash: hash }}/>
}
......@@ -72,4 +90,4 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }
);
};
export default React.memo(chakra(PrivateTagMenuItem));
export default React.memo(PrivateTagMenuItem);
import { MenuItem, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: () => boolean;
type: ItemType;
}
const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const PublicTagMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter();
const handleClick = React.useCallback(() => {
......@@ -21,12 +26,23 @@ const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/public-tags-request', query: { address: hash } });
}, [ hash, onBeforeClick, router ]);
return (
<MenuItem className={ className }onClick={ handleClick }>
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/>
<span>Add public tag</span>
</MenuItem>
);
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add public tag" icon="publictags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
);
}
}
})();
return element;
};
export default React.memo(chakra(PublicTagMenuItem));
export default React.memo(PublicTagMenuItem);
import { MenuItem, chakra, useDisclosure } from '@chakra-ui/react';
import { chakra, useDisclosure } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import type { Route } from 'nextjs-routes';
import config from 'configs/app';
......@@ -11,13 +13,17 @@ import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: (route: Route) => boolean;
type: ItemType;
}
const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const TokenInfoMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter();
const modal = useDisclosure();
const isAuth = useHasAccount();
......@@ -61,37 +67,40 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/verified-addresses' });
}, [ router ]);
const icon = <IconSvg name="edit" boxSize={ 6 } mr={ 2 } p={ 1 }/>;
const content = (() => {
if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase())) {
return (
<MenuItem className={ className } onClick={ handleAddAddressClick }>
{ icon }
<span>{ tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info' }</span>
</MenuItem>
);
}
const element = (() => {
const icon = <IconSvg name="edit" boxSize={ 6 } p={ 1 }/>;
const isVerifiedAddress = verifiedAddressesQuery.data?.verifiedAddresses
.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase());
const hasApplication = applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress.toLowerCase() === hash.toLowerCase());
return (
<MenuItem className={ className } onClick={ handleAddApplicationClick }>
{ icon }
<span>
{
hasApplication || tokenInfoQuery.data?.tokenAddress ?
'Update token info' :
'Add token info'
}
</span>
</MenuItem>
);
const label = (() => {
if (!isVerifiedAddress) {
return tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info';
}
return hasApplication || tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info';
})();
const onClick = isVerifiedAddress ? handleAddApplicationClick : handleAddAddressClick;
switch (type) {
case 'button': {
return <ButtonItem label={ label } icon={ icon } onClick={ onClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ onClick }>
{ icon }
<chakra.span ml={ 2 }>{ label }</chakra.span>
</MenuItem>
);
}
}
})();
return (
<>
{ content }
{ element }
<AddressVerificationModal
defaultAddress={ hash }
pageType={ PAGE_TYPE_DICT['/token/[hash]'] }
......@@ -105,4 +114,4 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
);
};
export default React.memo(chakra(TokenInfoMenuItem));
export default React.memo(TokenInfoMenuItem);
import { IconButton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
className?: string;
onClick: () => void;
label: string;
icon: IconName | React.ReactElement;
}
const ButtonItem = ({ className, label, onClick, icon }: Props) => {
return (
<Tooltip label={ label }>
<IconButton
aria-label={ label }
className={ className }
icon={ typeof icon === 'string' ? <IconSvg name={ icon } boxSize={ 6 }/> : icon }
onClick={ onClick }
size="sm"
variant="outline"
px="4px"
/>
</Tooltip>
);
};
export default ButtonItem;
import { MenuItem as MenuItemChakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
children: React.ReactNode;
onClick: () => void;
}
const MenuItem = ({ className, children, onClick }: Props) => {
return (
<MenuItemChakra className={ className } onClick={ onClick } py={ 2 } px={ 4 }>
{ children }
</MenuItemChakra>
);
};
export default MenuItem;
export type ItemType = 'button' | 'menu_item';
export interface ItemProps {
type: ItemType;
hash: string;
onBeforeClick: () => boolean;
}
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