feat(trading): view as button in sidebar (#4470)

This commit is contained in:
Art 2023-08-03 13:14:35 +02:00 committed by GitHub
parent 8a47a92dbd
commit cd6e906dc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 48 deletions

View File

@ -1,7 +1,15 @@
import { act, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Sidebar, SidebarContent, ViewType, useSidebar } from './sidebar'; import {
Sidebar,
SidebarButton,
SidebarContent,
ViewType,
useSidebar,
} from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({ jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />, NodeHealthContainer: () => <span data-testid="node-health" />,
@ -28,11 +36,14 @@ describe('Sidebar', () => {
'does not render ticket and info', 'does not render ticket and info',
(path) => { (path) => {
render( render(
<MemoryRouter initialEntries={[path]}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={[path]}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@ -47,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => { it('renders ticket and info on market pages', () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@ -65,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => { it('renders selected state', async () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
const settingsButton = screen.getByTestId(ViewType.Settings); const settingsButton = screen.getByTestId(ViewType.Settings);
@ -91,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => { describe('SidebarContent', () => {
it('renders the correct content', () => { it('renders the correct content', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/markets/ABC']}>
<Route path="/markets/:marketId" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/markets/:marketId" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
@ -115,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => { it('closes sidebar if market id is required but not present', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/portfolio']}>
<Route path="/portfolio" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/portfolio" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
act(() => { act(() => {
@ -141,3 +161,25 @@ describe('SidebarContent', () => {
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });
describe('SidebarButton', () => {
it.each([ViewType.Info, ViewType.Deposit, ViewType.ViewAs])(
'runs given callback regardless of requested view (%s)',
async (view) => {
const onClick = jest.fn();
render(
<SidebarButton
icon={VegaIconNames.INFO}
tooltip="INFO"
onClick={onClick}
view={view}
/>
);
const btn = screen.getByTestId(view);
await userEvent.click(btn);
expect(onClick).toBeCalled();
}
);
});

View File

@ -16,6 +16,7 @@ import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router'; import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog'; import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store'; const STORAGE_KEY = 'vega_sidebar_store';
@ -26,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw', Withdraw = 'Withdraw',
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs',
} }
type SidebarView = type SidebarView =
@ -53,6 +55,8 @@ type SidebarView =
export const Sidebar = () => { export const Sidebar = () => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
return ( return (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar"> <div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}> <nav className={navClasses}>
@ -105,6 +109,16 @@ export const Sidebar = () => {
</Routes> </Routes>
</nav> </nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
/>
<SidebarButton <SidebarButton
view={ViewType.Settings} view={ViewType.Settings}
icon={VegaIconNames.COG} icon={VegaIconNames.COG}
@ -116,25 +130,41 @@ export const Sidebar = () => {
); );
}; };
const SidebarButton = ({ export const SidebarButton = ({
view, view,
icon, icon,
tooltip, tooltip,
disabled = false,
onClick,
}: { }: {
view: ViewType; view?: ViewType;
icon: VegaIconNames; icon: VegaIconNames;
tooltip: string; tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => { }) => {
const { currView, setView } = useSidebar((store) => ({ const { currView, setView } = useSidebar((store) => ({
currView: store.view, currView: store.view,
setView: store.setView, setView: store.setView,
})); }));
const onSelect = (view: SidebarView['type']) => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
};
const buttonClasses = classNames('flex items-center p-1 rounded', { const buttonClasses = classNames('flex items-center p-1 rounded', {
'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500': 'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500':
view !== currView?.type, !view || view !== currView?.type,
'bg-vega-yellow hover:bg-vega-yellow-550 text-black': 'bg-vega-yellow hover:bg-vega-yellow-550 text-black':
view === currView?.type, view && view === currView?.type,
'cursor-not-allowed text-vega-clight-500 hover:bg-inherit dark:text-vega-cdark-500 dark:hover:bg-inherit':
disabled,
}); });
return ( return (
<Tooltip <Tooltip
description={tooltip} description={tooltip}
@ -146,13 +176,8 @@ const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={() => { onClick={onClick || (() => onSelect(view as SidebarView['type']))}
if (view === currView?.type) { disabled={disabled}
setView(null);
} else {
setView({ type: view });
}
}}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
</button> </button>
@ -282,7 +307,7 @@ export const useSidebar = create<{
view: null, view: null,
setView: (x) => setView: (x) =>
set(() => { set(() => {
if (x === null) { if (x == null) {
return { view: null, init: false }; return { view: null, init: false };
} }

View File

@ -2,7 +2,7 @@ import {
AssetDetailsDialog, AssetDetailsDialog,
useAssetDetailsDialogStore, useAssetDetailsDialogStore,
} from '@vegaprotocol/assets'; } from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet'; import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors'; import { Connectors } from '../lib/vega-connectors';
import { import {
Web3ConnectUncontrolledDialog, Web3ConnectUncontrolledDialog,
@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors} connectors={Connectors}
riskMessage={<RiskMessage />} riskMessage={<RiskMessage />}
/> />
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog <AssetDetailsDialog
assetId={id} assetId={id}
trigger={trigger || null} trigger={trigger || null}

View File

@ -0,0 +1,8 @@
export const IconEye = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M0.678395 7.44128C2.71173 4.52663 5.34817 2.99829 7.9894 2.99829C11.0914 2.99829 13.7635 5.01915 15.34 7.45404L15.3408 7.45519C15.4451 7.61729 15.5006 7.80599 15.5006 7.99876C15.5006 8.19118 15.4453 8.37954 15.3414 8.54144C13.7662 11.0077 11.1114 12.9983 7.9894 12.9983C4.8342 12.9983 2.23054 11.0118 0.659261 8.55277C0.552495 8.38698 0.497119 8.19332 0.500115 7.99613C0.503119 7.79845 0.564651 7.6061 0.676935 7.44338L0.67839 7.44127L0.678395 7.44128ZM1.5 8.01133L1.50139 8.01348L1.50139 8.01349C2.95267 10.2852 5.27875 11.9983 7.9894 11.9983C10.6697 11.9983 13.0459 10.278 14.4989 8.00262L14.4999 8.00113C14.5001 8.00073 14.5003 8.0003 14.5005 7.99985C14.5005 7.9995 14.5006 7.99913 14.5006 7.99876C14.5006 7.99792 14.5003 7.9971 14.4999 7.99639L14.9203 7.72579L14.5006 7.99754C13.0421 5.74493 10.6461 3.99829 7.9894 3.99829C5.75997 3.99829 3.40017 5.28866 1.5 8.01133Z" />
<path d="M8 5.99951C6.89543 5.99951 6 6.89494 6 7.99951C6 9.10408 6.89543 9.99951 8 9.99951C9.10457 9.99951 10 9.10408 10 7.99951C10 6.89494 9.10457 5.99951 8 5.99951ZM5 7.99951C5 6.34266 6.34315 4.99951 8 4.99951C9.65685 4.99951 11 6.34266 11 7.99951C11 9.65637 9.65685 10.9995 8 10.9995C6.34315 10.9995 5 9.65637 5 7.99951Z" />
</svg>
);
};

View File

@ -12,6 +12,7 @@ import { IconCross } from './svg-icons/icon-cross';
import { IconDeposit } from './svg-icons/icon-deposit'; import { IconDeposit } from './svg-icons/icon-deposit';
import { IconEdit } from './svg-icons/icon-edit'; import { IconEdit } from './svg-icons/icon-edit';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark'; import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconEye } from './svg-icons/icon-eye';
import { IconForum } from './svg-icons/icon-forum'; import { IconForum } from './svg-icons/icon-forum';
import { IconGlobe } from './svg-icons/icon-globe'; import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info'; import { IconInfo } from './svg-icons/icon-info';
@ -48,6 +49,7 @@ export enum VegaIconNames {
DEPOSIT = 'deposit', DEPOSIT = 'deposit',
EDIT = 'edit', EDIT = 'edit',
EXCLAIMATION_MARK = 'exclaimation-mark', EXCLAIMATION_MARK = 'exclaimation-mark',
EYE = 'eye',
FORUM = 'forum', FORUM = 'forum',
GLOBE = 'globe', GLOBE = 'globe',
INFO = 'info', INFO = 'info',
@ -88,6 +90,7 @@ export const VegaIconNameMap: Record<
deposit: IconDeposit, deposit: IconDeposit,
edit: IconEdit, edit: IconEdit,
'exclaimation-mark': IconExclaimationMark, 'exclaimation-mark': IconExclaimationMark,
eye: IconEye,
forum: IconForum, forum: IconForum,
globe: IconGlobe, globe: IconGlobe,
info: IconInfo, info: IconInfo,

View File

@ -21,7 +21,7 @@ export const NotificationBanner = ({
return ( return (
<div <div
className={classNames( className={classNames(
'flex items-center p-3 border-b min-h-[56px]', 'flex items-center px-1 py-3 border-b min-h-[56px]',
'text-[12px] leading-[16px] font-normal', 'text-[12px] leading-[16px] font-normal',
{ {
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None, 'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,

View File

@ -8,7 +8,7 @@ import type {
import { Intent } from '../../utils/intent'; import { Intent } from '../../utils/intent';
type TradingButtonProps = { type TradingButtonProps = {
size?: 'large' | 'medium' | 'small'; size?: 'large' | 'medium' | 'small' | 'extra-small';
intent?: Intent; intent?: Intent;
children?: ReactNode; children?: ReactNode;
icon?: ReactNode; icon?: ReactNode;
@ -31,6 +31,7 @@ const getClassName = (
'h-10': !subLabel && (!size || size === 'medium'), 'h-10': !subLabel && (!size || size === 'medium'),
'h-8': !subLabel && size === 'small', 'h-8': !subLabel && size === 'small',
'px-3 text-sm': !subLabel && size === 'small', 'px-3 text-sm': !subLabel && size === 'small',
'h-6 px-2 text-xs': !subLabel && size === 'extra-small',
'px-4 text-base': !subLabel && size !== 'small', 'px-4 text-base': !subLabel && size !== 'small',
'flex-col items-center justify-center px-3 pt-2.5 pb-2': subLabel, 'flex-col items-center justify-center px-3 pt-2.5 pb-2': subLabel,
}, },

View File

@ -1,6 +1,7 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { NotificationBanner } from '../notification-banner'; import { NotificationBanner } from '../notification-banner';
import { Intent } from '../../utils/intent'; import { Intent } from '../../utils/intent';
import { TradingButton } from '../trading-button';
export function truncateMiddle(address: string, start = 6, end = 4) { export function truncateMiddle(address: string, start = 6, end = 4) {
if (address.length < 11) return address; if (address.length < 11) return address;
@ -24,19 +25,20 @@ export const ViewingAsBanner = ({
<NotificationBanner <NotificationBanner
data-testid="view-banner" data-testid="view-banner"
intent={Intent.None} intent={Intent.None}
className="py-2 min-h-fit" className="!px-1 !py-1 min-h-fit"
> >
<div className="flex justify-between items-baseline"> <div className="flex justify-between items-baseline">
<span> <span>
{t('Viewing as Vega user:')} {pubKey && truncateMiddle(pubKey)}{' '} {t('Viewing as Vega user:')} {pubKey && truncateMiddle(pubKey)}{' '}
</span> </span>
<button <TradingButton
className="p-2 bg-light-dark-150 dark:bg-vega-dark-150 rounded uppercase" intent={Intent.None}
size="extra-small"
data-testid="exit-view" data-testid="exit-view"
onClick={disconnect} onClick={disconnect}
> >
{t('Exit view as')} {t('Exit view as')}
</button> </TradingButton>
</div> </div>
</NotificationBanner> </NotificationBanner>
); );

View File

@ -1 +1,2 @@
export * from './connect-dialog'; export * from './connect-dialog';
export * from './view-as-dialog';

View File

@ -0,0 +1,40 @@
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { ViewConnectorForm } from './view-connector-form';
import type { ViewConnector } from '../connectors';
import { create } from 'zustand';
type ViewAsDialogStore = {
open: boolean;
setOpen: (open: boolean) => void;
};
export const useViewAsDialog = create<ViewAsDialogStore>()((set) => ({
open: false,
setOpen: (open) => set({ open }),
}));
type ViewAsDialogProps = {
connector: ViewConnector;
};
export const ViewAsDialog = ({ connector }: ViewAsDialogProps) => {
const open = useViewAsDialog((state) => state.open);
const setOpen = useViewAsDialog((state) => state.setOpen);
return (
<Dialog
open={open}
size="small"
onChange={(open) => {
setOpen(open);
}}
>
<ViewConnectorForm
connector={connector}
onConnect={() => {
setOpen(false);
}}
/>
</Dialog>
);
};

View File

@ -2,9 +2,10 @@ import { t } from '@vegaprotocol/i18n';
import { import {
Button, Button,
FormGroup, FormGroup,
Icon,
Input, Input,
InputError, InputError,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import type { ViewConnector } from '../connectors'; import type { ViewConnector } from '../connectors';
@ -17,7 +18,7 @@ interface FormFields {
interface RestConnectorFormProps { interface RestConnectorFormProps {
connector: ViewConnector; connector: ViewConnector;
onConnect: (connector: ViewConnector) => void; onConnect: (connector: ViewConnector) => void;
reset: () => void; reset?: () => void;
} }
export function ViewConnectorForm({ export function ViewConnectorForm({
@ -50,13 +51,19 @@ export function ViewConnectorForm({
return ( return (
<> <>
<button {reset && (
onClick={reset} <button
className="absolute p-2 top-0 left-0 md:top-2 md:left-2" onClick={reset}
data-testid="back-button" className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
> data-testid="back-button"
<Icon name={'chevron-left'} ariaLabel="back" size={4} /> >
</button> <VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
aria-label="back"
size={16}
/>
</button>
)}
<form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form"> <form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form">
<h1 className="text-2xl uppercase mb-6 text-center font-alpha calt"> <h1 className="text-2xl uppercase mb-6 text-center font-alpha calt">
{t('VIEW AS VEGA USER')} {t('VIEW AS VEGA USER')}