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 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 { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />,
@ -28,11 +36,14 @@ describe('Sidebar', () => {
'does not render ticket and info',
(path) => {
render(
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@ -47,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@ -65,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
const settingsButton = screen.getByTestId(ViewType.Settings);
@ -91,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => {
it('renders the correct content', () => {
const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
expect(container).toBeEmptyDOMElement();
@ -115,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => {
const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
act(() => {
@ -141,3 +161,25 @@ describe('SidebarContent', () => {
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 { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store';
@ -26,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw',
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
}
type SidebarView =
@ -53,6 +55,8 @@ type SidebarView =
export const Sidebar = () => {
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 (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}>
@ -105,6 +109,16 @@ export const Sidebar = () => {
</Routes>
</nav>
<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
view={ViewType.Settings}
icon={VegaIconNames.COG}
@ -116,25 +130,41 @@ export const Sidebar = () => {
);
};
const SidebarButton = ({
export const SidebarButton = ({
view,
icon,
tooltip,
disabled = false,
onClick,
}: {
view: ViewType;
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => {
const { currView, setView } = useSidebar((store) => ({
currView: store.view,
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', {
'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':
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 (
<Tooltip
description={tooltip}
@ -146,13 +176,8 @@ const SidebarButton = ({
<button
className={buttonClasses}
data-testid={view}
onClick={() => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
}}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
</button>
@ -282,7 +307,7 @@ export const useSidebar = create<{
view: null,
setView: (x) =>
set(() => {
if (x === null) {
if (x == null) {
return { view: null, init: false };
}

View File

@ -2,7 +2,7 @@ import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import {
Web3ConnectUncontrolledDialog,
@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors}
riskMessage={<RiskMessage />}
/>
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog
assetId={id}
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 { IconEdit } from './svg-icons/icon-edit';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconEye } from './svg-icons/icon-eye';
import { IconForum } from './svg-icons/icon-forum';
import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info';
@ -48,6 +49,7 @@ export enum VegaIconNames {
DEPOSIT = 'deposit',
EDIT = 'edit',
EXCLAIMATION_MARK = 'exclaimation-mark',
EYE = 'eye',
FORUM = 'forum',
GLOBE = 'globe',
INFO = 'info',
@ -88,6 +90,7 @@ export const VegaIconNameMap: Record<
deposit: IconDeposit,
edit: IconEdit,
'exclaimation-mark': IconExclaimationMark,
eye: IconEye,
forum: IconForum,
globe: IconGlobe,
info: IconInfo,

View File

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

View File

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