feat(2146): adjust and refactor welcome dialogs (#2384)

* feat: adjust and refactor welcome dialogs

* feat: adjust and refactor welcome dialogs - add int tests

* feat: adjust and refactor welcome dialogs - small fixes and imprvments

* feat: adjust and refactor welcome dialogs - fix a typo

* feat: adjust and refactor welcome dialogs - fix a property name

* feat: adjust and refactor welcome dialogs - fix an unit test
This commit is contained in:
macqbat 2022-12-13 14:31:28 +01:00 committed by GitHub
parent 01addaea7c
commit c14e57cfd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 659 additions and 399 deletions

View File

@ -86,6 +86,14 @@ describe('home', { tags: '@regression' }, () => {
});
}
});
cy.getByTestId('welcome-notice-proposed-markets')
.children('div.pt-1.flex.justify-between')
.should('have.length', 3)
.each((item) => {
cy.wrap(item).getByTestId('external-link').should('exist');
});
cy.getByTestId('dialog-close').click();
cy.getByTestId(selectMarketOverlay).should('not.exist');
@ -96,7 +104,7 @@ describe('home', { tags: '@regression' }, () => {
});
});
describe('market table should properly rendered', () => {
describe('market table should be properly rendered', () => {
it('redirects to a default market with the landing dialog open', () => {
const override = {
marketsConnection: {
@ -161,4 +169,59 @@ describe('home', { tags: '@regression' }, () => {
);
});
});
describe('no proposal found', () => {
it('there is a link to propose market', () => {
cy.mockGQL((req) => {
aliasQuery(req, 'ProposalsList', {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: null,
},
});
});
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.getByTestId(selectMarketOverlay)
.get('table tr')
.then((row) => {
expect(row.length >= 3).to.be.true;
});
cy.getByTestId('external-link')
.contains('Propose a market')
.should('exist');
});
});
describe('no proposal nor markets found', () => {
it('there are welcome text and a link to propose market', () => {
cy.mockGQL((req) => {
const data = {
marketsConnection: {
__typename: 'MarketConnection',
edges: [],
},
};
aliasQuery(req, 'Markets', data);
aliasQuery(req, 'MarketsData', data);
aliasQuery(req, 'ProposalsList', {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: null,
},
});
});
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.getByTestId('welcome-notice-title').should(
'contain.text',
'Welcome to Console'
);
cy.getByTestId('external-link')
.contains('Propose a market')
.should('exist');
});
});
});

View File

@ -17,8 +17,7 @@ export const Home = () => {
const { data, error, loading } = useDataProvider({
dataProvider: marketsWithDataProvider,
});
const { riskNoticeDialog, update } = useGlobalStore((store) => ({
riskNoticeDialog: store.riskNoticeDialog,
const { update } = useGlobalStore((store) => ({
update: store.update,
}));
@ -29,7 +28,6 @@ export const Home = () => {
useEffect(() => {
if (data) {
update({ landingDialog: data.length > 0 });
const marketId = data[0]?.id;
const marketName = data[0]?.tradableInstrument.instrument.name;
const marketPrice = data[0]?.data?.markPrice
@ -50,7 +48,7 @@ export const Home = () => {
navigate(`/markets/${EMPTY_MARKET_ID}`);
}
}
}, [data, navigate, riskNoticeDialog, update, pageTitle, updateTitle]);
}, [data, navigate, update, pageTitle, updateTitle]);
return (
<AsyncRenderer data={data} loading={loading} error={error}>

View File

@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import debounce from 'lodash/debounce';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
addDecimalsFormatNumber,
t,
@ -18,10 +17,8 @@ import type {
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid, TradePanels } from './trade-grid';
import { ColumnKind, SelectMarketDialog } from '../../components/select-market';
import { useNavigate, useParams } from 'react-router-dom';
import { EMPTY_MARKET_ID } from '../../components/constants';
import { useWelcomeNoticeDialog } from '../../components/welcome-notice';
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
return markPrice && decimalPlaces
@ -47,21 +44,15 @@ export const Market = ({
const marketId = isEmpty ? undefined : params.marketId;
const { w } = useWindowSize();
const { landingDialog, riskNoticeDialog, update } = useGlobalStore(
(store) => ({
landingDialog: store.landingDialog,
riskNoticeDialog: store.riskNoticeDialog,
const { update } = useGlobalStore((store) => ({
update: store.update,
})
);
}));
const { pageTitle, updateTitle } = usePageTitleStore((store) => ({
pageTitle: store.pageTitle,
updateTitle: store.updateTitle,
}));
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const onSelect = useCallback(
(id: string) => {
if (id && id !== marketId) {
@ -113,8 +104,6 @@ export const Market = ({
skip: !marketId || !data,
});
useWelcomeNoticeDialog();
const tradeView = useMemo(() => {
if (w > 960) {
return <TradeGrid market={data} onSelect={onSelect} />;
@ -140,23 +129,7 @@ export const Market = ({
if (!data && !isEmpty) {
return <Splash>{t('Market not found')}</Splash>;
}
return (
<>
{tradeView}
<SelectMarketDialog
dialogOpen={landingDialog && !riskNoticeDialog}
setDialogOpen={(isOpen: boolean) =>
update({ landingDialog: isOpen })
}
onSelect={onSelect}
onCellClick={(e, kind, value) => {
if (value && kind === ColumnKind.Asset) {
openAssetDetailsDialog(value, e.target as HTMLElement);
}
}}
/>
</>
);
return <>{tradeView}</>;
}}
/>
);

View File

@ -2,15 +2,13 @@ import { useCallback } from 'react';
import { MarketsContainer } from '@vegaprotocol/market-list';
import { useGlobalStore } from '../../stores';
import { useNavigate } from 'react-router-dom';
import { useWelcomeNoticeDialog } from '../../components/welcome-notice';
export const Markets = () => {
const navigate = useNavigate();
const { update } = useGlobalStore((store) => ({ update: store.update }));
useWelcomeNoticeDialog();
const handleOnSelect = useCallback(
(marketId: string) => {
update({ marketId, welcomeNoticeDialog: false });
update({ marketId });
navigate(`/markets/${marketId}`);
},
[update, navigate]

View File

@ -1,2 +1,10 @@
import { t } from '@vegaprotocol/react-helpers';
export const DEBOUNCE_UPDATE_TIME = 500;
export const EMPTY_MARKET_ID = 'empty';
export const RISK_ACCEPTED_KEY = 'vega-risk-accepted';
export const MAINNET_WELCOME_HEADER = t(
'Trade cash settled futures on the fully decentralised Vega network.'
);
export const TESTNET_WELCOME_HEADER = t(
'Try out trading cash settled futures on the fully decentralised Vega network (Testnet).'
);

View File

@ -1 +0,0 @@
export * from './risk-notice-dialog';

View File

@ -1,10 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import {
SelectAllMarketsTableBody,
SelectMarketLandingTable,
} from './select-market';
import { SelectAllMarketsTableBody } from './select-market';
import type {
MarketWithCandles,
@ -173,24 +170,4 @@ describe('SelectMarket', () => {
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onSelect).toHaveBeenCalledWith('1');
});
it('should call onSelect callback on SelectMarketLandingTable', () => {
const onSelect = jest.fn();
const onCellClick = jest.fn();
render(
<MemoryRouter>
<SelectMarketLandingTable
markets={[MARKET_A as Market, MARKET_B as Market]}
onCellClick={onCellClick}
onSelect={onSelect}
/>
</MemoryRouter>,
{ wrapper: MockedProvider }
);
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onSelect).toHaveBeenCalledWith('1');
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
expect(onSelect).toHaveBeenCalledWith('2');
});
});

View File

@ -1,18 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import {
Dialog,
ExternalLink,
Icon,
Intent,
Link as UILink,
Loader,
Popover,
} from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
columnHeaders,
columnHeadersPositionMarkets,
@ -24,7 +15,6 @@ import {
SelectMarketTableRow,
SelectMarketTableRowSplash,
} from './select-market-table';
import type { ReactNode } from 'react';
import type {
MarketWithCandles,
@ -32,7 +22,6 @@ import type {
} from '@vegaprotocol/market-list';
import type { PositionFieldsFragment } from '@vegaprotocol/positions';
import type { Column, OnCellClickHandler } from './select-market-columns';
import { Link } from 'react-router-dom';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
@ -40,48 +29,7 @@ import {
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
type Market = MarketWithCandles & MarketWithData;
export const SelectMarketLandingTable = ({
markets,
onSelect,
onCellClick,
}: {
markets: Market[] | null;
onSelect: (id: string) => void;
onCellClick: OnCellClickHandler;
}) => {
return (
<>
<div
className="max-h-[60vh] overflow-x-auto"
data-testid="select-market-list"
>
<table className="text-sm relative h-full min-w-full whitespace-nowrap">
<thead className="sticky top-0 z-10 bg-white dark:bg-black">
<SelectMarketTableHeader />
</thead>
<tbody>
{markets?.map((market, i) => (
<SelectMarketTableRow
marketId={market.id}
key={i}
detailed={false}
onSelect={onSelect}
columns={columns(market, onSelect, onCellClick)}
/>
))}
</tbody>
</table>
</div>
<div className="mt-4 text-md">
<Link to="/markets" data-testid="view-market-list-link">
<UILink>{'Or view full market list'} </UILink>
</Link>
</div>
</>
);
};
export type Market = MarketWithCandles & MarketWithData;
export const SelectAllMarketsTableBody = ({
markets,
@ -265,71 +213,3 @@ const TableTitle = ({ children }: { children: ReactNode }) => {
</thead>
);
};
export const SelectMarketDialog = ({
dialogOpen,
setDialogOpen,
onSelect,
onCellClick,
}: {
dialogOpen: boolean;
setDialogOpen: (open: boolean) => void;
title?: string;
onSelect: (id: string) => void;
onCellClick: OnCellClickHandler;
}) => {
const onSelectMarket = (id: string) => {
onSelect(id);
setDialogOpen(false);
};
return (
<Dialog
title={t('Select a market to get started')}
intent={Intent.Primary}
open={dialogOpen}
onChange={() => setDialogOpen(false)}
size="small"
>
<LandingDialogContainer
onSelect={onSelectMarket}
onCellClick={onCellClick}
/>
</Dialog>
);
};
interface LandingDialogContainerProps {
onSelect: (id: string) => void;
onCellClick: OnCellClickHandler;
}
const LandingDialogContainer = ({
onSelect,
onCellClick,
}: LandingDialogContainerProps) => {
const { data, loading, error } = useMarketList();
if (error) {
return (
<div className="flex justify-center items-center">
<p className="my-8">{t('Failed to load markets')}</p>
</div>
);
}
if (loading) {
return (
<div className="flex justify-center items-center">
<p className="my-8">{t('Loading...')}</p>
</div>
);
}
return (
<SelectMarketLandingTable
markets={data}
onSelect={onSelect}
onCellClick={onCellClick}
/>
);
};

View File

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

View File

@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { proposalsListDataProvider } from '@vegaprotocol/governance';
import take from 'lodash/take';
import * as Types from '@vegaprotocol/types';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
export const ProposedMarkets = () => {
const variables = useMemo(() => {
return {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
};
}, []);
const { data } = useDataProvider({
dataProvider: proposalsListDataProvider,
variables,
skipUpdates: true,
});
const newMarkets = take(
(data || []).filter((proposal) =>
[
Types.ProposalState.STATE_OPEN,
Types.ProposalState.STATE_PASSED,
Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
].includes(proposal.state)
),
3
).map((proposal) => ({
id: proposal.id,
displayName:
proposal.terms.change.__typename === 'NewMarket' &&
proposal.terms.change.instrument.code,
}));
const tokenLink = useLinks(DApp.Token);
return useMemo(
() => (
<div className="mt-7 pt-8 border-t border-neutral-700">
{newMarkets.length > 0 ? (
<>
<h2 className="font-alpha uppercase text-2xl">
{t('Proposed markets')}
</h2>
<dl data-testid="welcome-notice-proposed-markets" className="py-5">
{newMarkets.map(({ displayName, id }, i) => (
<div className="pt-1 flex justify-between" key={i}>
<dl>{displayName}</dl>
<dt>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL.replace(':id', id || ''))}
>
{t('View or vote')}
</ExternalLink>
</dt>
</div>
))}
</dl>
<ExternalLink href={tokenLink(TOKEN_PROPOSALS)}>
{t('View all proposed markets')}
</ExternalLink>
</>
) : (
<ExternalLink href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}>
{t('Propose a market')}
</ExternalLink>
)}
</div>
),
[newMarkets, tokenLink]
);
};

View File

@ -1,15 +1,9 @@
import { BrowserRouter } from 'react-router-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import { RiskNoticeDialog } from './risk-notice-dialog';
import { MockedProvider } from '@apollo/client/testing';
import { Networks, EnvironmentProvider } from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
beforeEach(() => {
localStorage.clear();
useGlobalStore.setState((state) => ({
...state,
riskNoticeDialog: false,
}));
});
import { RiskNoticeDialog } from './risk-notice-dialog';
import { WelcomeDialog } from './welcome-dialog';
const mockEnvDefinitions = {
VEGA_CONFIG_URL: 'https://config.url',
@ -18,6 +12,12 @@ const mockEnvDefinitions = {
};
describe('Risk notice dialog', () => {
const introText = 'Regulation may apply to use of this app';
const mockOnClose = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
it.each`
assertion | network
${'displays'} | ${Networks.MAINNET}
@ -33,46 +33,39 @@ describe('Risk notice dialog', () => {
definitions={{ ...mockEnvDefinitions, VEGA_ENV: network }}
config={{ hosts: [] }}
>
<RiskNoticeDialog />
</EnvironmentProvider>
<MockedProvider>
<WelcomeDialog />
</MockedProvider>
</EnvironmentProvider>,
{ wrapper: BrowserRouter }
);
if (assertion === 'displays') {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.queryByText('WARNING')).toBeInTheDocument();
expect(screen.queryByText(introText)).toBeInTheDocument();
} else {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.queryByText('WARNING')).not.toBeInTheDocument();
expect(screen.queryByText(introText)).not.toBeInTheDocument();
}
}
);
it("doesn't display the risk notice when previously acknowledged", () => {
const { rerender } = render(
render(
<EnvironmentProvider
definitions={{ ...mockEnvDefinitions, VEGA_ENV: Networks.MAINNET }}
config={{ hosts: [] }}
>
<RiskNoticeDialog />
<RiskNoticeDialog onClose={mockOnClose} />
</EnvironmentProvider>
);
expect(screen.queryByText('WARNING')).toBeInTheDocument();
expect(screen.queryByText(introText)).toBeInTheDocument();
const button = screen.getByRole('button', {
name: 'I understand, Continue',
});
fireEvent.click(button);
rerender(
<EnvironmentProvider
definitions={{ ...mockEnvDefinitions, VEGA_ENV: Networks.MAINNET }}
config={{ hosts: [] }}
>
<RiskNoticeDialog />
</EnvironmentProvider>
);
expect(screen.queryByText('WARNING')).not.toBeInTheDocument();
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@ -1,34 +1,20 @@
import { useEffect } from 'react';
import { useCallback } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { Dialog, Button } from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import { LocalStorage } from '@vegaprotocol/react-helpers';
import { useEnvironment, Networks } from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
import { RISK_ACCEPTED_KEY } from '../constants';
export const RISK_ACCEPTED_KEY = 'vega-risk-accepted';
export const RiskNoticeDialog = () => {
const { riskNoticeDialog, update } = useGlobalStore((store) => ({
riskNoticeDialog: store.riskNoticeDialog,
update: store.update,
}));
const { VEGA_ENV } = useEnvironment();
useEffect(() => {
const isRiskAccepted = LocalStorage.getItem(RISK_ACCEPTED_KEY) === 'true';
if (!isRiskAccepted && VEGA_ENV === Networks.MAINNET) {
update({ riskNoticeDialog: true });
interface Props {
onClose: () => void;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [update, VEGA_ENV]);
const handleAcceptRisk = () => {
update({ riskNoticeDialog: false });
export const RiskNoticeDialog = ({ onClose }: Props) => {
const handleAcceptRisk = useCallback(() => {
onClose();
LocalStorage.setItem(RISK_ACCEPTED_KEY, 'true');
};
}, [onClose]);
return (
<Dialog open={riskNoticeDialog} title={t('WARNING')} size="medium">
<>
<h4 className="text-xl mb-2 mt-4">
{t('Regulation may apply to use of this app')}
</h4>
@ -46,6 +32,6 @@ export const RiskNoticeDialog = () => {
)}
</p>
<Button onClick={handleAcceptRisk}>{t('I understand, Continue')}</Button>
</Dialog>
</>
);
};

View File

@ -0,0 +1,11 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import * as constants from '../constants';
export const WelcomeDialogHeader = () => {
const { VEGA_ENV } = useEnvironment();
const header =
VEGA_ENV === Networks.MAINNET
? constants.MAINNET_WELCOME_HEADER
: constants.TESTNET_WELCOME_HEADER;
return <h1 className="mb-6 p-4 text-center text-2xl">{header}</h1>;
};

View File

@ -0,0 +1,70 @@
import React, { useMemo, useState, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import {
t,
useDataProvider,
useLocalStorage,
} from '@vegaprotocol/react-helpers';
import { activeMarketsProvider } from '@vegaprotocol/market-list';
import { useEnvironment, Networks } from '@vegaprotocol/environment';
import * as constants from '../constants';
import { RiskNoticeDialog } from './risk-notice-dialog';
import { WelcomeNoticeDialog } from './welcome-notice-dialog';
import { WelcomeLandingDialog } from './welcome-landing-dialog';
interface DialogConfig {
open?: boolean;
content: React.ReactNode;
title?: string;
size?: 'small' | 'medium';
onClose: () => void;
}
export const WelcomeDialog = () => {
const { pathname } = useLocation();
const { VEGA_ENV } = useEnvironment();
const [dialog, setDialog] = useState<DialogConfig | null>(null);
const onClose = useCallback(() => {
setDialog(null);
}, [setDialog]);
const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY);
const { data } = useDataProvider({
dataProvider: activeMarketsProvider,
});
useMemo(() => {
switch (true) {
case riskAccepted !== 'true' && VEGA_ENV === Networks.MAINNET:
setDialog({
content: <RiskNoticeDialog onClose={onClose} />,
title: t('WARNING'),
size: 'medium',
onClose,
});
break;
case pathname === '/' && data?.length === 0:
setDialog({
content: <WelcomeNoticeDialog />,
onClose,
});
break;
case pathname === '/' && (data?.length || 0) > 0:
setDialog({
content: <WelcomeLandingDialog onClose={onClose} />,
onClose,
});
break;
}
}, [onClose, data?.length, riskAccepted, pathname, VEGA_ENV, setDialog]);
return dialog ? (
<Dialog
open={Boolean(dialog.content)}
title={dialog.title}
size={dialog.size}
onChange={dialog.onClose}
>
{dialog.content}
</Dialog>
) : null;
};

View File

@ -0,0 +1,169 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import * as Schema from '@vegaprotocol/types';
import type {
MarketWithCandles,
MarketWithData,
MarketData,
} from '@vegaprotocol/market-list';
import { SelectMarketLandingTable } from './welcome-landing-dialog';
type Market = MarketWithCandles & MarketWithData;
type PartialMarket = Partial<
Omit<Market, 'data'> & { data: Partial<MarketData> }
>;
const MARKET_A: PartialMarket = {
__typename: 'Market',
id: '1',
decimalPlaces: 2,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '1',
code: 'ABCDEF',
name: 'ABCDEF 1-Day',
product: {
__typename: 'Future',
quoteName: 'ABCDEF',
settlementAsset: {
__typename: 'Asset',
id: 'asset-ABC',
decimals: 2,
symbol: 'ABC',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['ABC', 'DEF'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '1',
},
markPrice: '90',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '1000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
{
__typename: 'Candle',
high: '10',
low: '1',
open: '1',
close: '100',
volume: '1000',
periodStart: '2022-11-01T15:50:00Z',
},
],
};
const MARKET_B: PartialMarket = {
__typename: 'Market',
id: '2',
decimalPlaces: 2,
positionDecimalPlaces: 0,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '2',
code: 'XYZ',
name: 'XYZ 1-Day',
product: {
__typename: 'Future',
quoteName: 'XYZ',
settlementAsset: {
__typename: 'Asset',
id: 'asset-XYZ',
decimals: 2,
symbol: 'XYZ',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['XYZ'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '2',
},
markPrice: '123.123',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '2000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
],
};
describe('WelcomeLandingDialog', () => {
it('should call onSelect callback on SelectMarketLandingTable', () => {
const onClose = jest.fn();
render(
<MemoryRouter>
<SelectMarketLandingTable
markets={[MARKET_A as Market, MARKET_B as Market]}
onClose={onClose}
/>
</MemoryRouter>,
{ wrapper: MockedProvider }
);
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onClose).toHaveBeenCalled();
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
expect(onClose).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,132 @@
import React, { useCallback } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { t } from '@vegaprotocol/react-helpers';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Link as UILink } from '@vegaprotocol/ui-toolkit';
import type { Market, OnCellClickHandler } from '../select-market';
import {
ColumnKind,
columns,
SelectMarketTableHeader,
SelectMarketTableRow,
} from '../select-market';
import { WelcomeDialogHeader } from './welcome-dialog-header';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { EMPTY_MARKET_ID } from '../constants';
import { useGlobalStore } from '../../stores';
import { ProposedMarkets } from './proposed-markets';
export const SelectMarketLandingTable = ({
markets,
onClose,
}: {
markets: Market[] | null;
onClose: () => void;
}) => {
const params = useParams();
const navigate = useNavigate();
const isEmpty = params.marketId === EMPTY_MARKET_ID;
const marketId = isEmpty ? undefined : params.marketId;
const { update } = useGlobalStore((store) => ({
update: store.update,
}));
const onSelect = useCallback(
(id: string) => {
if (id && id !== marketId) {
update({ marketId: id });
navigate(`/markets/${id}`);
}
},
[marketId, update, navigate]
);
const onSelectMarket = useCallback(
(id: string) => {
onSelect(id);
onClose();
},
[onSelect, onClose]
);
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const onCellClick = useCallback<OnCellClickHandler>(
(e, kind, value) => {
if (value && kind === ColumnKind.Asset) {
openAssetDetailsDialog(value, e.target as HTMLElement);
}
},
[openAssetDetailsDialog]
);
const showProposed = (markets?.length || 0) <= 5;
return (
<>
<div
className="max-h-[60vh] overflow-x-auto"
data-testid="select-market-list"
>
<p className="text-neutral-500 dark:text-neutral-400">
{t('Select a market to get started...')}
</p>
<table className="text-sm relative h-full min-w-full whitespace-nowrap">
<thead className="sticky top-0 z-10 bg-white dark:bg-black">
<SelectMarketTableHeader />
</thead>
<tbody>
{markets?.map((market, i) => (
<SelectMarketTableRow
marketId={market.id}
key={i}
detailed={false}
onSelect={onSelectMarket}
columns={columns(market, onSelect, onCellClick)}
/>
))}
</tbody>
</table>
</div>
<div className="mt-4 text-md">
<Link
to="/markets"
data-testid="view-market-list-link"
onClick={() => onClose()}
>
<UILink>{'Or view full market list'} </UILink>
</Link>
</div>
{showProposed && <ProposedMarkets />}
</>
);
};
interface LandingDialogContainerProps {
onClose: () => void;
}
export const WelcomeLandingDialog = ({
onClose,
}: LandingDialogContainerProps) => {
const { data, loading, error } = useMarketList();
if (error) {
return (
<div className="flex justify-center items-center">
<p className="my-8">{t('Failed to load markets')}</p>
</div>
);
}
if (loading) {
return (
<div className="flex justify-center items-center">
<p className="my-8">{t('Loading...')}</p>
</div>
);
}
return (
<>
<WelcomeDialogHeader />
<SelectMarketLandingTable markets={data} onClose={onClose} />
</>
);
};

View File

@ -0,0 +1,66 @@
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import {
BLOG,
DApp,
Networks,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSALS,
useEnvironment,
useLinks,
} from '@vegaprotocol/environment';
import { ProposedMarkets } from './proposed-markets';
export const WelcomeNoticeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET);
const isMainnet = VEGA_ENV === Networks.MAINNET;
const networkName = isMainnet ? 'mainnet' : 'testnet';
return (
<>
<h1
data-testid="welcome-notice-title"
className="mb-6 p-4 text-center text-2xl"
>
{t('Welcome to Console')}
</h1>
<p className="leading-6 mb-7">
{t(
'Vega %s is now live, but markets need to be voted for before the can be traded on. In the meantime:',
[networkName]
)}
</p>
<ul className="list-[square] pl-7">
{isMainnet && (
<li>
<ExternalLink target="_blank" href={consoleFairgroundLink()}>
{t('Try out Console')}
</ExternalLink>
{t(' on Fairground, our Testnet')}
</li>
)}
<li>
<ExternalLink target="_blank" href={tokenLink(TOKEN_PROPOSALS)}>
{t('View and vote for proposed markets')}
</ExternalLink>
</li>
<li>
<ExternalLink
target="_blank"
href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}
>
{t('Propose your own markets')}
</ExternalLink>
</li>
<li>
<ExternalLink target="_blank" href={BLOG}>
{t('Read about the mainnet launch')}
</ExternalLink>
</li>
</ul>
<ProposedMarkets />
</>
);
};

View File

@ -1 +0,0 @@
export * from './welcome-notice-dialog';

View File

@ -1,146 +0,0 @@
import { Dialog, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { proposalsListDataProvider } from '@vegaprotocol/governance';
import * as Types from '@vegaprotocol/types';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { useCallback, useEffect, useMemo } from 'react';
import { useGlobalStore } from '../../stores';
import take from 'lodash/take';
import { activeMarketsProvider } from '@vegaprotocol/market-list';
import {
BLOG,
DApp,
Networks,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
export const WelcomeNoticeDialog = () => {
const [welcomeNoticeDialog, update] = useGlobalStore((store) => [
store.welcomeNoticeDialog,
store.update,
]);
const onOpenChange = useCallback(
(isOpen: boolean) => {
update({ welcomeNoticeDialog: isOpen });
},
[update]
);
const variables = useMemo(() => {
return {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
};
}, []);
const { data } = useDataProvider({
dataProvider: proposalsListDataProvider,
variables,
skipUpdates: true,
});
const newMarkets = take(
(data || []).filter((proposal) =>
[
Types.ProposalState.STATE_OPEN,
Types.ProposalState.STATE_PASSED,
Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
].includes(proposal.state)
),
3
).map((proposal) => ({
id: proposal.id,
displayName:
proposal.terms.change.__typename === 'NewMarket' &&
proposal.terms.change.instrument.code,
}));
const tokenLink = useLinks(DApp.Token);
const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET);
const proposedMarkets = useMemo(
() =>
newMarkets.length > 0 && (
<div className="mt-7 pt-8 border-t border-neutral-700">
<h2 className="font-alpha uppercase text-2xl">
{t('Proposed markets')}
</h2>
<dl data-testid="welcome-notice-proposed-markets" className="py-5">
{newMarkets.map(({ displayName, id }, i) => (
<div className="pt-1 flex justify-between" key={i}>
<dl>{displayName}</dl>
<dt>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL.replace(':id', id || ''))}
>
{t('View or vote')}
</ExternalLink>
</dt>
</div>
))}
</dl>
<ExternalLink href={tokenLink(TOKEN_PROPOSALS)}>
{t('View all proposed markets')}
</ExternalLink>
</div>
),
[newMarkets, tokenLink]
);
return (
<Dialog open={welcomeNoticeDialog} onChange={onOpenChange}>
<h1
data-testid="welcome-notice-title"
className="font-alpha uppercase text-4xl mb-7 mt-5"
>
{t('Welcome to Console')}
</h1>
<p className="leading-6 mb-7">
{t(
'Vega mainnet is now live, but markets need to be voted for before the can be traded on. In the meantime:'
)}
</p>
<ul className="list-[square] pl-7">
<li>
<ExternalLink target="_blank" href={consoleFairgroundLink()}>
{t('Try out Console')}
</ExternalLink>
{t(' on Fairground, our Testnet')}
</li>
<li>
<ExternalLink target="_blank" href={tokenLink(TOKEN_PROPOSALS)}>
{t('View and vote for proposed markets')}
</ExternalLink>
</li>
<li>
<ExternalLink
target="_blank"
href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}
>
{t('Propose your own markets')}
</ExternalLink>
</li>
<li>
<ExternalLink target="_blank" href={BLOG}>
{t('Read about the mainnet launch')}
</ExternalLink>
</li>
</ul>
{proposedMarkets}
</Dialog>
);
};
export const useWelcomeNoticeDialog = () => {
const { update } = useGlobalStore((store) => ({ update: store.update }));
const { data } = useDataProvider({
dataProvider: activeMarketsProvider,
});
useEffect(() => {
if (data?.length === 0) {
update({ welcomeNoticeDialog: true });
}
}, [data, update]);
};

View File

@ -4,11 +4,10 @@ import {
} from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import { RiskNoticeDialog } from '../components/risk-notice-dialog';
import { WithdrawalDialog } from '@vegaprotocol/withdraws';
import { DepositDialog } from '@vegaprotocol/deposits';
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
import { WelcomeNoticeDialog } from '../components/welcome-notice';
import { WelcomeDialog } from '../components/welcome-dialog';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
@ -21,11 +20,10 @@ const DialogsContainer = () => {
open={isOpen}
onChange={setOpen}
/>
<RiskNoticeDialog />
<WelcomeDialog />
<DepositDialog />
<Web3ConnectUncontrolledDialog />
<WithdrawalDialog />
<WelcomeNoticeDialog />
</>
);
};

View File

@ -1,4 +1,5 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;

View File

@ -3,10 +3,7 @@ import create from 'zustand';
interface GlobalStore {
networkSwitcherDialog: boolean;
landingDialog: boolean;
riskNoticeDialog: boolean;
marketId: string | null;
welcomeNoticeDialog: boolean;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
}
@ -17,10 +14,7 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>((set) => ({
networkSwitcherDialog: false,
landingDialog: false,
riskNoticeDialog: false,
marketId: LocalStorage.getItem('marketId') || null,
welcomeNoticeDialog: false,
update: (state) => {
set(state);
if (state.marketId) {

View File

@ -6,4 +6,15 @@
* @param str A
* @returns str A
*/
export const t = (str: string) => str;
export const t = (str: string, replacements?: string | string[]) => {
if (replacements) {
let i = 0;
return str.replace(/%s/g, () => {
return (
(Array.isArray(replacements) ? replacements : [replacements])[i++] ||
'%s'
);
});
}
return str;
};