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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { ItemProps } from './types';
import config from 'configs/app'; import config from 'configs/app';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed'; import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
...@@ -33,11 +35,42 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { ...@@ -33,11 +35,42 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
return null; 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 ( return (
<Menu> <Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base" className={ className }>
<MenuButton <MenuButton
as={ IconButton } as={ IconButton }
className={ className }
size="sm" size="sm"
variant="outline" variant="outline"
colorScheme="gray" colorScheme="gray"
...@@ -45,18 +78,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { ...@@ -45,18 +78,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
onClick={ handleButtonClick } onClick={ handleButtonClick }
icon={ <IconSvg name="dots" boxSize="18px"/> } icon={ <IconSvg name="dots" boxSize="18px"/> }
/> />
</Skeleton>
<MenuList minWidth="180px" zIndex="popover"> <MenuList minWidth="180px" zIndex="popover">
{ isTokenPage && config.features.addressVerification.isEnabled && { items.map(({ render }, index) => (
<TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> } <React.Fragment key={ index }>
<PrivateTagMenuItem { render({ type: 'menu_item', hash, onBeforeClick: isAccountActionAllowed }) }
py={ 2 } </React.Fragment>
px={ 4 } )) }
hash={ hash }
onBeforeClick={ isAccountActionAllowed }
type={ isTxPage ? 'tx' : 'address' }
/>
{ !isTxPage && <PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
</MenuList> </MenuList>
</Menu> </Menu>
); );
......
import { MenuItem, chakra, useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { ItemType } from '../types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -12,19 +13,23 @@ import AddressModal from 'ui/privateTags/AddressModal/AddressModal'; ...@@ -12,19 +13,23 @@ import AddressModal from 'ui/privateTags/AddressModal/AddressModal';
import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal'; import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
onBeforeClick: () => boolean; 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 modal = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); 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 queryData = queryClient.getQueryData<Address | Transaction>(queryKey);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
...@@ -58,13 +63,26 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' } ...@@ -58,13 +63,26 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }
pageType, pageType,
}; };
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add private tag" icon="privattags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return ( return (
<>
<MenuItem className={ className } onClick={ handleClick }> <MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/> <IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span> <span>Add private tag</span>
</MenuItem> </MenuItem>
{ type === 'tx' ? );
}
}
})();
return (
<>
{ element }
{ entityType === 'tx' ?
<TransactionModal { ...modalProps } data={{ transaction_hash: hash }}/> : <TransactionModal { ...modalProps } data={{ transaction_hash: hash }}/> :
<AddressModal { ...modalProps } data={{ address_hash: hash }}/> <AddressModal { ...modalProps } data={{ address_hash: hash }}/>
} }
...@@ -72,4 +90,4 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' } ...@@ -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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { ItemType } from '../types';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
onBeforeClick: () => boolean; onBeforeClick: () => boolean;
type: ItemType;
} }
const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { const PublicTagMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter(); const router = useRouter();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
...@@ -21,12 +26,23 @@ const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { ...@@ -21,12 +26,23 @@ const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/public-tags-request', query: { address: hash } }); router.push({ pathname: '/account/public-tags-request', query: { address: hash } });
}, [ hash, onBeforeClick, router ]); }, [ hash, onBeforeClick, router ]);
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add public tag" icon="publictags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return ( return (
<MenuItem className={ className }onClick={ handleClick }> <MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/> <IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/>
<span>Add public tag</span> <span>Add private tag</span>
</MenuItem> </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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { ItemType } from '../types';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
...@@ -11,13 +13,17 @@ import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType'; ...@@ -11,13 +13,17 @@ import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
onBeforeClick: (route: Route) => boolean; onBeforeClick: (route: Route) => boolean;
type: ItemType;
} }
const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => { const TokenInfoMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter(); const router = useRouter();
const modal = useDisclosure(); const modal = useDisclosure();
const isAuth = useHasAccount(); const isAuth = useHasAccount();
...@@ -61,37 +67,40 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => { ...@@ -61,37 +67,40 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/verified-addresses' }); router.push({ pathname: '/account/verified-addresses' });
}, [ router ]); }, [ router ]);
const icon = <IconSvg name="edit" boxSize={ 6 } mr={ 2 } p={ 1 }/>; 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());
const content = (() => { const label = (() => {
if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase())) { if (!isVerifiedAddress) {
return ( return tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info';
<MenuItem className={ className } onClick={ handleAddAddressClick }>
{ icon }
<span>{ tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info' }</span>
</MenuItem>
);
} }
const hasApplication = applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress.toLowerCase() === hash.toLowerCase()); 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 ( return (
<MenuItem className={ className } onClick={ handleAddApplicationClick }> <MenuItem className={ className } onClick={ onClick }>
{ icon } { icon }
<span> <chakra.span ml={ 2 }>{ label }</chakra.span>
{
hasApplication || tokenInfoQuery.data?.tokenAddress ?
'Update token info' :
'Add token info'
}
</span>
</MenuItem> </MenuItem>
); );
}
}
})(); })();
return ( return (
<> <>
{ content } { element }
<AddressVerificationModal <AddressVerificationModal
defaultAddress={ hash } defaultAddress={ hash }
pageType={ PAGE_TYPE_DICT['/token/[hash]'] } pageType={ PAGE_TYPE_DICT['/token/[hash]'] }
...@@ -105,4 +114,4 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => { ...@@ -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