feat(trading): view as button in sidebar (#4470)
This commit is contained in:
parent
8a47a92dbd
commit
cd6e906dc6
@ -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(
|
||||
<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(
|
||||
<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(
|
||||
<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(
|
||||
<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(
|
||||
<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();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './connect-dialog';
|
||||
export * from './view-as-dialog';
|
||||
|
40
libs/wallet/src/connect-dialog/view-as-dialog.tsx
Normal file
40
libs/wallet/src/connect-dialog/view-as-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<>
|
||||
{reset && (
|
||||
<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} />
|
||||
<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')}
|
||||
|
Loading…
Reference in New Issue
Block a user