feat: add a global zustand store for managing dialogs (#494)

* feat: add a global zustand store for managing connect dialogs and landing dialog

* feat: add tests

* fix: remove condition for cypress for auto connecting

* chore: fix assertion in tests for vega wallet text

* fix: add mock for landing dialog markets query

Co-authored-by: madalinaraicu <madalina@vegaprotocol.io>
This commit is contained in:
Matthew Russell 2022-06-01 07:21:36 -07:00 committed by GitHub
parent e63e17f173
commit 25b67009a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 329 additions and 99 deletions

View File

@ -0,0 +1,77 @@
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import type { MarketList, MarketList_markets } from '@vegaprotocol/market-list';
export const generateMarketList = (
override?: PartialDeep<MarketList>
): MarketList => {
const markets: MarketList_markets[] = [
{
id: 'market-id',
decimalPlaces: 5,
data: {
market: {
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
__typename: 'Market',
},
markPrice: '4612690058',
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
name: 'BTC/USD Monthly',
code: 'BTCUSD.MF21',
metadata: {
__typename: 'InstrumentMetadata',
tags: ['tag1'],
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '',
close: '',
},
candles: [{ __typename: 'Candle', open: '100', close: '100' }],
__typename: 'Market',
},
{
id: 'test-market-suspended',
decimalPlaces: 2,
data: {
market: {
id: '34d95e10faa00c21d19d382d6d7e6fc9722a96985369f0caec041b0f44b775ed',
__typename: 'Market',
},
markPrice: '8441',
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
name: 'SOL/USD',
code: 'SOLUSD',
metadata: {
__typename: 'InstrumentMetadata',
tags: ['tag1'],
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '',
close: '',
},
candles: [{ __typename: 'Candle', open: '100', close: '100' }],
__typename: 'Market',
},
];
const defaultResult = {
markets,
};
return merge(defaultResult, override);
};

View File

@ -31,7 +31,7 @@ export default class WithdrawalsPage extends BasePage {
validateConnectWalletText() {
cy.getByTestId(this.connectVegaWalletText).should(
'have.text',
'Please connect your Vega wallet'
'Connect your Vega wallet'
);
}

View File

@ -1,9 +1,18 @@
import { Given } from 'cypress-cucumber-preprocessor/steps';
import { hasOperationName } from '..';
import { generateMarketList } from '../mocks/generate-market-list';
import BasePage from '../pages/base-page';
const basePage = new BasePage();
Given('I am on the homepage', () => {
cy.mockGQL('MarketsList', (req) => {
if (hasOperationName(req, 'MarketsList')) {
req.reply({
body: { data: generateMarketList() },
});
}
});
cy.visit('/');
basePage.closeDialog();
});

View File

@ -0,0 +1 @@
export * from './vega-wallet-container';

View File

@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react';
import { VegaWalletContainer } from './vega-wallet-container';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { PartialDeep } from 'type-fest';
const generateJsx = (context: PartialDeep<VegaWalletContextShape>) => {
return (
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<VegaWalletContainer>
<div data-testid="child" />
</VegaWalletContainer>
</VegaWalletContext.Provider>
);
};
describe('VegaWalletContainer', () => {
it('doesnt render children if not connected', () => {
render(generateJsx({ keypair: null }));
expect(screen.queryByTestId('child')).not.toBeInTheDocument();
});
it('renders children if connected', () => {
render(generateJsx({ keypair: { pub: '0x123' } }));
expect(screen.getByTestId('child')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,34 @@
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useGlobalStore } from '../../stores';
interface VegaWalletContainerProps {
children: ReactNode;
}
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const store = useGlobalStore();
const { keypair } = useVegaWallet();
if (!keypair) {
return (
<Splash>
<div className="text-center">
<p className="mb-12" data-testid="connect-vega-wallet-text">
{t('Connect your Vega wallet')}
</p>
<Button
onClick={() => store.setVegaWalletConnectDialog(true)}
data-testid="vega-wallet-connect"
>
{t('Connect')}
</Button>
</div>
</Splash>
);
}
return <>{children}</>;
};

View File

@ -108,11 +108,7 @@ export const Web3Content = ({
const { isActive, error, connector, chainId } = useWeb3React();
useEffect(() => {
if (
connector?.connectEagerly &&
// Dont eager connect if this is a cypress test run
'Cypress' in window
) {
if (connector?.connectEagerly) {
connector.connectEagerly();
}
}, [connector]);

View File

@ -9,20 +9,18 @@ import {
} from '@vegaprotocol/wallet';
import { EnvironmentProvider } from '@vegaprotocol/react-helpers';
import { Connectors } from '../lib/vega-connectors';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { createClient } from '../lib/apollo-client';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { ApolloProvider } from '@apollo/client';
import { AppLoader } from '../components/app-loader';
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
import './styles.css';
import { useGlobalStore } from '../stores';
function VegaTradingApp({ Component, pageProps }: AppProps) {
const client = useMemo(() => createClient(process.env['NX_VEGA_URL']), []);
const [vegaWallet, setVegaWallet] = useState({
connect: false,
manage: false,
});
const store = useGlobalStore();
const [theme, toggleTheme] = useThemeSwitcher();
return (
@ -55,12 +53,12 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
<Navbar />
<div className="flex items-center gap-4 ml-auto mr-8">
<VegaWalletConnectButton
setConnectDialog={(open) =>
setVegaWallet((x) => ({ ...x, connect: open }))
}
setManageDialog={(open) =>
setVegaWallet((x) => ({ ...x, manage: open }))
}
setConnectDialog={(open) => {
store.setVegaWalletConnectDialog(open);
}}
setManageDialog={(open) => {
store.setVegaWalletManageDialog(open);
}}
/>
<ThemeSwitcher onToggle={toggleTheme} className="-my-4" />
</div>
@ -71,15 +69,15 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
</main>
<VegaConnectDialog
connectors={Connectors}
dialogOpen={vegaWallet.connect}
dialogOpen={store.vegaWalletConnectDialog}
setDialogOpen={(open) =>
setVegaWallet((x) => ({ ...x, connect: open }))
store.setVegaWalletConnectDialog(open)
}
/>
<VegaManageDialog
dialogOpen={vegaWallet.manage}
dialogOpen={store.vegaWalletManageDialog}
setDialogOpen={(open) =>
setVegaWallet((x) => ({ ...x, manage: open }))
store.setVegaWalletManageDialog(open)
}
/>
</div>

View File

@ -1,9 +1,10 @@
import { gql, useQuery } from '@apollo/client';
import { LandingDialog } from '@vegaprotocol/market-list';
import { MarketTradingMode } from '@vegaprotocol/types';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import sortBy from 'lodash/sortBy';
import MarketPage from './markets/[marketId].page';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useGlobalStore } from '../stores';
import type { MarketsLanding } from './__generated__/MarketsLanding';
const MARKETS_QUERY = gql`
@ -29,24 +30,33 @@ const marketList = ({ markets }: MarketsLanding) =>
);
export function Index() {
const { replace } = useRouter();
// The default market selected in the platform behind the overlay
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
const { data, error, loading } = useQuery<MarketsLanding>(MARKETS_QUERY);
if (data && !error && !loading) {
const marketId = marketList(data)[0]?.id;
window.history.replaceState(
data,
'',
marketId ? `/markets/${marketId}` : '/markets'
);
}
const setLandingDialog = useGlobalStore((state) => state.setLandingDialog);
useEffect(() => {
if (data) {
const marketId = marketList(data)[0]?.id;
// If a default market is found, go to it with the landing dialog open
if (marketId) {
setLandingDialog(true);
replace(`/markets/${marketId}`);
}
// Fallback to the markets list page
else {
replace('/markets');
}
}
}, [data, replace, setLandingDialog]);
return (
<>
<LandingDialog />
<AsyncRenderer data={data} error={error} loading={loading}>
<MarketPage id={data && marketList(data)[0]?.id} />
</AsyncRenderer>
</>
<AsyncRenderer data={data} loading={loading} error={error}>
{/* Render a loading and error state but we will redirect if markets are found */}
{null}
</AsyncRenderer>
);
}

View File

@ -7,6 +7,8 @@ import debounce from 'lodash/debounce';
import { PageQueryContainer } from '../../components/page-query-container';
import { TradeGrid, TradePanels } from './trade-grid';
import { t } from '@vegaprotocol/react-helpers';
import { useGlobalStore } from '../../stores';
import { LandingDialog } from '@vegaprotocol/market-list';
// Top level page query
const MARKET_QUERY = gql`
@ -21,6 +23,7 @@ const MARKET_QUERY = gql`
const MarketPage = ({ id }: { id?: string }) => {
const { query } = useRouter();
const { w } = useWindowSize();
const store = useGlobalStore();
// Default to first marketId query item if found
const marketId =
@ -48,10 +51,18 @@ const MarketPage = ({ id }: { id?: string }) => {
return <Splash>{t('Market not found')}</Splash>;
}
return w > 960 ? (
<TradeGrid market={market} />
) : (
<TradePanels market={market} />
return (
<>
{w > 960 ? (
<TradeGrid market={market} />
) : (
<TradePanels market={market} />
)}
<LandingDialog
open={store.landingDialog}
setOpen={(isOpen) => store.setLandingDialog(isOpen)}
/>
</>
);
}}
/>

View File

@ -1,8 +1,9 @@
import { Web3Container } from '../../../components/web3-container';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { WithdrawPageContainer } from './withdraw-page-container';
import { t } from '@vegaprotocol/react-helpers';
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
import { Web3Container } from '../../../components/web3-container';
const Withdraw = () => {
const { query } = useRouter();
@ -21,14 +22,16 @@ const Withdraw = () => {
}, [query]);
return (
<Web3Container
render={() => (
<div className="max-w-[420px] p-24 mx-auto">
<h1 className="text-h3 mb-12">{t('Withdraw')}</h1>
<WithdrawPageContainer assetId={assetId} />
</div>
)}
/>
<VegaWalletContainer>
<Web3Container
render={() => (
<div className="max-w-[420px] p-24 mx-auto">
<h1 className="text-h3 mb-12">{t('Withdraw')}</h1>
<WithdrawPageContainer assetId={assetId} />
</div>
)}
/>
</VegaWalletContainer>
);
};

View File

@ -47,14 +47,6 @@ export const WithdrawPageContainer = ({
}: WithdrawPageContainerProps) => {
const { keypair } = useVegaWallet();
if (!keypair) {
return (
<p data-testid="connect-vega-wallet-text">
{t('Please connect your Vega wallet')}
</p>
);
}
return (
<PageQueryContainer<WithdrawPageQuery, WithdrawPageQueryVariables>
query={WITHDRAW_PAGE_QUERY}

View File

@ -1,30 +1,26 @@
import { t } from '@vegaprotocol/react-helpers';
import { AnchorButton, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
import { Web3Container } from '../../../components/web3-container';
import { WithdrawalsPageContainer } from './withdrawals-page-container';
const Withdrawals = () => {
const { keypair } = useVegaWallet();
if (!keypair) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<Web3Container
render={() => (
<div className="h-full grid grid grid-rows-[min-content,1fr]">
<header className="flex justify-between p-24">
<h1 className="text-h3">{t('Withdrawals')}</h1>
<AnchorButton href="/portfolio/withdraw">
{t('Start withdrawal')}
</AnchorButton>
</header>
<WithdrawalsPageContainer />
</div>
)}
/>
<VegaWalletContainer>
<Web3Container
render={() => (
<div className="h-full grid grid grid-rows-[min-content,1fr]">
<header className="flex justify-between p-24">
<h1 className="text-h3">{t('Withdrawals')}</h1>
<AnchorButton href="/portfolio/withdraw">
{t('Start withdrawal')}
</AnchorButton>
</header>
<WithdrawalsPageContainer />
</div>
)}
/>
</VegaWalletContainer>
);
};

View File

@ -0,0 +1,26 @@
import type { SetState } from 'zustand';
import create from 'zustand';
interface GlobalStore {
vegaWalletConnectDialog: boolean;
setVegaWalletConnectDialog: (isOpen: boolean) => void;
vegaWalletManageDialog: boolean;
setVegaWalletManageDialog: (isOpen: boolean) => void;
landingDialog: boolean;
setLandingDialog: (isOpen: boolean) => void;
}
export const useGlobalStore = create((set: SetState<GlobalStore>) => ({
vegaWalletConnectDialog: false,
setVegaWalletConnectDialog: (isOpen: boolean) => {
set({ vegaWalletConnectDialog: isOpen });
},
vegaWalletManageDialog: false,
setVegaWalletManageDialog: (isOpen: boolean) => {
set({ vegaWalletManageDialog: isOpen });
},
landingDialog: false,
setLandingDialog: (isOpen: boolean) => {
set({ landingDialog: isOpen });
},
}));

View File

@ -0,0 +1 @@
export * from './global';

View File

@ -2,14 +2,17 @@ import { useQuery } from '@apollo/client';
import { t } from '@vegaprotocol/react-helpers';
import { Interval } from '@vegaprotocol/types';
import { AsyncRenderer, Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
import { MARKET_LIST_QUERY } from '../markets-container/markets-data-provider';
import type { MarketList } from '../markets-container/__generated__/MarketList';
import { SelectMarketList } from './select-market-list';
export const LandingDialog = () => {
const [open, setOpen] = useState(true);
interface LandingDialogProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export const LandingDialog = ({ open, setOpen }: LandingDialogProps) => {
const setClose = () => setOpen(false);
const yesterday = Math.round(new Date().getTime() / 1000) - 24 * 3600;
@ -21,17 +24,15 @@ export const LandingDialog = () => {
return (
<AsyncRenderer loading={loading} error={error} data={data}>
{
<Dialog
title={t('Select a market to get started')}
intent={Intent.Prompt}
open={open}
onChange={setClose}
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
>
<SelectMarketList data={data} />
</Dialog>
}
<Dialog
title={t('Select a market to get started')}
intent={Intent.Prompt}
open={open}
onChange={setClose}
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
>
<SelectMarketList data={data} onSelect={setClose} />
</Dialog>
</AsyncRenderer>
);
};

View File

@ -1,15 +1,41 @@
import { render, screen } from '@testing-library/react';
import type { MarketList } from '../__generated__/MarketList';
import { fireEvent, render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';
import type { MarketList } from '../markets-container/__generated__/MarketList';
import { SelectMarketList } from './select-market-list';
jest.mock(
'next/link',
() =>
({ children }: { children: ReactNode }) =>
children
);
describe('SelectMarketList', () => {
it('should render', () => {
render(<SelectMarketList data={mockData.data as MarketList} />);
render(
<SelectMarketList
data={mockData.data as MarketList}
onSelect={jest.fn()}
/>
);
expect(screen.getByText('AAPL.MF21')).toBeTruthy();
expect(screen.getByText('-3.14%')).toBeTruthy();
expect(screen.getByText('141.75')).toBeTruthy();
expect(screen.getByText('Or view full market list')).toBeTruthy();
});
it('should call onSelect callback', () => {
const onSelect = jest.fn();
const expectedMarket = mockData.data.markets[0];
render(
<SelectMarketList
data={mockData.data as MarketList}
onSelect={onSelect}
/>
);
fireEvent.click(screen.getByTestId(`market-link-${expectedMarket.id}`));
expect(onSelect).toHaveBeenCalledWith(expectedMarket.id);
});
});
const mockData = {

View File

@ -10,11 +10,12 @@ import type { MarketList } from '../markets-container/__generated__/MarketList';
export interface SelectMarketListProps {
data: MarketList | undefined;
onSelect: (id: string) => void;
}
type CandleClose = Required<string>;
export const SelectMarketList = ({ data }: SelectMarketListProps) => {
export const SelectMarketList = ({ data, onSelect }: SelectMarketListProps) => {
const thClassNames = (direction: 'left' | 'right') =>
`px-8 text-${direction} font-sans font-normal text-ui-small leading-9 mb-0 text-dark/80 dark:text-white/80`;
const tdClassNames =
@ -49,8 +50,15 @@ export const SelectMarketList = ({ data }: SelectMarketListProps) => {
<td className={`${boldUnderlineClassNames} relative`}>
<Link
href={`/markets/${id}?portfolio=orders&trade=orderbook&chart=candles`}
passHref={true}
>
{marketName}
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
onClick={() => onSelect(id)}
data-testid={`market-link-${id}`}
>
{marketName}
</a>
</Link>
</td>
<td className={tdClassNames}>

View File

@ -2,3 +2,4 @@ export * from './market-list-table';
export * from './markets-container';
export * from './markets-data-provider';
export * from './summary-cell';
export * from './__generated__/MarketList';

View File

@ -71,7 +71,8 @@
"tailwindcss": "^3.0.23",
"tslib": "^2.0.0",
"uuid": "^8.3.2",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"zustand": "^4.0.0-rc.1"
},
"devDependencies": {
"@apollo/react-testing": "^4.0.0",

View File

@ -21214,6 +21214,11 @@ use-sync-external-store@1.0.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0.tgz#d98f4a9c2e73d0f958e7e2d2c2bfb5f618cbd8fd"
integrity sha512-AFVsxg5GkFg8GDcxnl+Z0lMAz9rE8DGJCc28qnBuQF7lac57B5smLcT37aXpXIIPz75rW4g3eXHPjhHwdGskOw==
use-sync-external-store@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@ -22130,6 +22135,13 @@ zustand@^4.0.0-beta.2:
dependencies:
use-sync-external-store "1.0.0"
zustand@^4.0.0-rc.1:
version "4.0.0-rc.1"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.0.0-rc.1.tgz#ec30a3afc03728adec7e1bd7bcc3592176372201"
integrity sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==
dependencies:
use-sync-external-store "1.1.0"
zwitch@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"