Feat/358 market list toolbar (#545)
* feat: [simple-app] - simple market list toolbar - first commit * feat: [simple-app] - simple market list toolbar - small improvements, unit test * feat: [simple-app] - simple market list toolbar - add some more unit tests * feat: [simple-app] - simple market list toolbar - add some e2e tests * feat: [simple-app] - simple market list toolbar - fixes after review feedback Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
741fd1f8af
commit
2d77059d93
@ -0,0 +1,65 @@
|
|||||||
|
describe('market list', () => {
|
||||||
|
describe('simple url', () => {
|
||||||
|
beforeEach(() => cy.visit('/markets'));
|
||||||
|
|
||||||
|
it('selects menus', () => {
|
||||||
|
cy.get('.MuiDrawer-root [aria-current]').should('have.text', 'Markets');
|
||||||
|
cy.get('select[name="states"]').should('have.value', 'Active');
|
||||||
|
cy.get('[data-testid="market-assets-menu"] button.font-bold').should(
|
||||||
|
'have.text',
|
||||||
|
'All'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigation should make possibly shortest url', () => {
|
||||||
|
cy.location('pathname').should('equal', '/markets');
|
||||||
|
|
||||||
|
cy.get('select[name="states"]').select('All');
|
||||||
|
cy.location('pathname').should('equal', '/markets/all');
|
||||||
|
|
||||||
|
cy.getByTestId('market-assets-menu')
|
||||||
|
.children()
|
||||||
|
.last()
|
||||||
|
.find('button')
|
||||||
|
.click();
|
||||||
|
cy.location('pathname').should('equal', '/markets/all/tEURO');
|
||||||
|
|
||||||
|
cy.get('button').contains('Future').click();
|
||||||
|
cy.location('pathname').should('equal', '/markets/all/tEURO/Future');
|
||||||
|
|
||||||
|
cy.get('button').contains('All Markets').click();
|
||||||
|
cy.location('pathname').should('equal', '/markets/all/tEURO');
|
||||||
|
|
||||||
|
cy.getByTestId('market-assets-menu')
|
||||||
|
.children()
|
||||||
|
.find('button')
|
||||||
|
.contains('All')
|
||||||
|
.click();
|
||||||
|
cy.location('pathname').should('equal', '/markets/all');
|
||||||
|
|
||||||
|
cy.get('select[name="states"]').select('Active');
|
||||||
|
cy.location('pathname').should('equal', '/markets');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('url params should select filters', () => {
|
||||||
|
it('suspended status', () => {
|
||||||
|
cy.visit('/markets/Suspended');
|
||||||
|
cy.get('select[name="states"]').should('have.value', 'Suspended');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tBTC asset', () => {
|
||||||
|
cy.visit('/markets/Suspended/tBTC');
|
||||||
|
cy.getByTestId('market-assets-menu')
|
||||||
|
.find('button.font-bold')
|
||||||
|
.should('have.text', 'tBTC');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Future product', () => {
|
||||||
|
cy.visit('/markets/Suspended/tBTC/Future');
|
||||||
|
cy.getByTestId('market-products-menu')
|
||||||
|
.find('button.active')
|
||||||
|
.should('have.text', 'Future');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -13,5 +13,6 @@
|
|||||||
// https://on.cypress.io/configuration
|
// https://on.cypress.io/configuration
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
|
import '@vegaprotocol/cypress';
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands';
|
import './commands';
|
||||||
|
55
apps/simple-trading-app/src/app/components/simple-market-list/__generated__/MarketFilters.ts
generated
Normal file
55
apps/simple-trading-app/src/app/components/simple-market-list/__generated__/MarketFilters.ts
generated
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: MarketFilters
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface MarketFilters_markets_tradableInstrument_instrument_product_settlementAsset {
|
||||||
|
__typename: "Asset";
|
||||||
|
/**
|
||||||
|
* The symbol of the asset (e.g: GBP)
|
||||||
|
*/
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketFilters_markets_tradableInstrument_instrument_product {
|
||||||
|
__typename: "Future";
|
||||||
|
/**
|
||||||
|
* The name of the asset (string)
|
||||||
|
*/
|
||||||
|
settlementAsset: MarketFilters_markets_tradableInstrument_instrument_product_settlementAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketFilters_markets_tradableInstrument_instrument {
|
||||||
|
__typename: "Instrument";
|
||||||
|
/**
|
||||||
|
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||||
|
*/
|
||||||
|
product: MarketFilters_markets_tradableInstrument_instrument_product;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketFilters_markets_tradableInstrument {
|
||||||
|
__typename: "TradableInstrument";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a fully specified instrument.
|
||||||
|
*/
|
||||||
|
instrument: MarketFilters_markets_tradableInstrument_instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketFilters_markets {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a tradable instrument.
|
||||||
|
*/
|
||||||
|
tradableInstrument: MarketFilters_markets_tradableInstrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketFilters {
|
||||||
|
/**
|
||||||
|
* One or more instruments that are trading on the VEGA network
|
||||||
|
*/
|
||||||
|
markets: MarketFilters_markets[] | null;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
export const MARKET_STATUS: Record<MarketState | '', Intent> = {
|
export const MARKET_STATUS: Record<MarketState | '', Intent> = {
|
||||||
[MarketState.Active]: Intent.Success,
|
[MarketState.Active]: Intent.Success,
|
||||||
@ -13,3 +14,16 @@ export const MARKET_STATUS: Record<MarketState | '', Intent> = {
|
|||||||
[MarketState.TradingTerminated]: Intent.Danger,
|
[MarketState.TradingTerminated]: Intent.Danger,
|
||||||
'': Intent.Primary,
|
'': Intent.Primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const STATES_FILTER = [
|
||||||
|
{ value: 'all', text: t('All') },
|
||||||
|
{ value: 'Active', text: t('Active') },
|
||||||
|
{ value: 'Cancelled', text: t('Cancelled') },
|
||||||
|
{ value: 'Closed', text: t('Closed') },
|
||||||
|
{ value: 'Pending', text: t('Pending') },
|
||||||
|
{ value: 'Proposed', text: t('Proposed') },
|
||||||
|
{ value: 'Rejected', text: t('Rejected') },
|
||||||
|
{ value: 'Settled', text: t('Settled') },
|
||||||
|
{ value: 'Suspended', text: t('Suspended') },
|
||||||
|
{ value: 'TradingTerminated', text: t('TradingTerminated') },
|
||||||
|
];
|
||||||
|
@ -33,6 +33,7 @@ export const MARKETS_QUERY = gql`
|
|||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
product {
|
product {
|
||||||
|
__typename
|
||||||
... on Future {
|
... on Future {
|
||||||
settlementAsset {
|
settlementAsset {
|
||||||
symbol
|
symbol
|
||||||
@ -66,6 +67,25 @@ export const CANDLE_SUB = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const FILTERS_QUERY = gql`
|
||||||
|
query MarketFilters {
|
||||||
|
markets {
|
||||||
|
tradableInstrument {
|
||||||
|
instrument {
|
||||||
|
product {
|
||||||
|
__typename
|
||||||
|
... on Future {
|
||||||
|
settlementAsset {
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const update = (
|
const update = (
|
||||||
draft: SimpleMarkets_markets[],
|
draft: SimpleMarkets_markets[],
|
||||||
delta: SimpleMarketDataSub_marketData
|
delta: SimpleMarketDataSub_marketData
|
||||||
|
@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"markets": [
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "XYZalpha",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tUSDC",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tBTC",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tEURO",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tEURO",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tBTC",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tUSDC",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tradableInstrument": {
|
||||||
|
"instrument": {
|
||||||
|
"product": {
|
||||||
|
"__typename": "Future",
|
||||||
|
"settlementAsset": {
|
||||||
|
"symbol": "tDAI",
|
||||||
|
"__typename": "Asset"
|
||||||
|
},
|
||||||
|
"__typename": "Future"
|
||||||
|
},
|
||||||
|
"__typename": "Instrument"
|
||||||
|
},
|
||||||
|
"__typename": "TradableInstrument"
|
||||||
|
},
|
||||||
|
"__typename": "Market"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,18 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
import SimpleMarketList from './simple-market-list';
|
import SimpleMarketList from './simple-market-list';
|
||||||
import { MARKETS_QUERY } from './data-provider';
|
import { FILTERS_QUERY, MARKETS_QUERY } from './data-provider';
|
||||||
import type { SimpleMarkets_markets } from './__generated__/SimpleMarkets';
|
import type {
|
||||||
import type { SimpleMarkets } from './__generated__/SimpleMarkets';
|
SimpleMarkets_markets,
|
||||||
|
SimpleMarkets,
|
||||||
|
} from './__generated__/SimpleMarkets';
|
||||||
|
import type { MarketFilters } from './__generated__/MarketFilters';
|
||||||
|
|
||||||
const mockedNavigate = jest.fn();
|
const mockedNavigate = jest.fn();
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useNavigate: () => mockedNavigate,
|
useNavigate: () => mockedNavigate,
|
||||||
|
useParams: () => ({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('date-fns', () => ({
|
jest.mock('date-fns', () => ({
|
||||||
@ -20,6 +25,15 @@ jest.mock('date-fns', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('SimpleMarketList', () => {
|
describe('SimpleMarketList', () => {
|
||||||
|
const filterMock: MockedResponse<MarketFilters> = {
|
||||||
|
request: {
|
||||||
|
query: FILTERS_QUERY,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: { markets: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@ -36,14 +50,14 @@ describe('SimpleMarketList', () => {
|
|||||||
data: { markets: [] },
|
data: { markets: [] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<MockedProvider mocks={[mocks]}>
|
<MockedProvider mocks={[mocks, filterMock]}>
|
||||||
<SimpleMarketList />
|
<SimpleMarketList />
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
});
|
||||||
|
|
||||||
expect(screen.getByText('No data to display')).toBeInTheDocument();
|
expect(screen.getByText('No data to display')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -74,7 +88,7 @@ describe('SimpleMarketList', () => {
|
|||||||
id: '2',
|
id: '2',
|
||||||
data: {
|
data: {
|
||||||
market: {
|
market: {
|
||||||
state: MarketState.Proposed,
|
state: MarketState.Active,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tradableInstrument: {
|
tradableInstrument: {
|
||||||
@ -103,15 +117,16 @@ describe('SimpleMarketList', () => {
|
|||||||
data: { markets: data },
|
data: { markets: data },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
render(
|
await act(async () => {
|
||||||
<MockedProvider mocks={[mocks]}>
|
render(
|
||||||
<SimpleMarketList />
|
<MockedProvider mocks={[mocks, filterMock]}>
|
||||||
</MockedProvider>
|
<SimpleMarketList />
|
||||||
);
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
expect(screen.getByTestId('simple-market-list')).toBeInTheDocument();
|
||||||
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
expect(screen.getByTestId('simple-market-list').children).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
@ -10,9 +10,19 @@ import SimpleMarketPercentChange from './simple-market-percent-change';
|
|||||||
import SimpleMarketExpires from './simple-market-expires';
|
import SimpleMarketExpires from './simple-market-expires';
|
||||||
import DataProvider from './data-provider';
|
import DataProvider from './data-provider';
|
||||||
import { MARKET_STATUS } from './constants';
|
import { MARKET_STATUS } from './constants';
|
||||||
|
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||||
|
import useMarketsFilterData from '../../hooks/use-markets-filter-data';
|
||||||
|
|
||||||
|
export type RouterParams = Partial<{
|
||||||
|
product: string;
|
||||||
|
asset: string;
|
||||||
|
state: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
const SimpleMarketList = () => {
|
const SimpleMarketList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const params = useParams<RouterParams>();
|
||||||
|
|
||||||
const statusesRef = useRef<Record<string, MarketState | ''>>({});
|
const statusesRef = useRef<Record<string, MarketState | ''>>({});
|
||||||
const variables = useMemo(
|
const variables = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -21,12 +31,7 @@ const SimpleMarketList = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(delta) => {
|
(delta) => statusesRef.current[delta.market.id] === delta.market.state,
|
||||||
if (statusesRef.current[delta.market.id] !== delta.market.state) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[statusesRef]
|
[statusesRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -35,13 +40,15 @@ const SimpleMarketList = () => {
|
|||||||
update,
|
update,
|
||||||
variables
|
variables
|
||||||
);
|
);
|
||||||
|
const localData = useMarketsFilterData(data || [], params);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const statuses: Record<string, MarketState | ''> = {};
|
const statuses: Record<string, MarketState | ''> = {};
|
||||||
data?.forEach((market) => {
|
data?.forEach((market) => {
|
||||||
statuses[market.id] = market.data?.market.state || '';
|
statuses[market.id] = market.data?.market.state || '';
|
||||||
});
|
});
|
||||||
statusesRef.current = statuses;
|
statusesRef.current = statuses;
|
||||||
}, [data]);
|
}, [data, statusesRef]);
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(marketId) => {
|
(marketId) => {
|
||||||
@ -51,55 +58,61 @@ const SimpleMarketList = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<>
|
||||||
{data && data.length > 0 ? (
|
<SimpleMarketToolbar />
|
||||||
<ul className="list-none relative pt-8 pb-8">
|
<AsyncRenderer loading={loading} error={error} data={localData}>
|
||||||
{data?.map((market) => (
|
{localData && localData.length > 0 ? (
|
||||||
<li
|
<ul
|
||||||
className="w-full relative flex justify-start items-center no-underline box-border text-left pt-8 pb-8 pl-16 pr-16 mb-10"
|
className="list-none relative pt-8 pb-8"
|
||||||
key={market.id}
|
data-testid="simple-market-list"
|
||||||
>
|
>
|
||||||
<div className="w-full grid sm:grid-cols-2">
|
{localData?.map((market) => (
|
||||||
<div className="w-full grid sm:auto-rows-auto">
|
<li
|
||||||
<div className="font-extrabold py-2">{market.name}</div>
|
className="w-full relative flex justify-start items-center no-underline box-border text-left pt-8 pb-8 pl-16 pr-16 mb-10"
|
||||||
<SimpleMarketExpires
|
key={market.id}
|
||||||
tags={market.tradableInstrument.instrument.metadata.tags}
|
>
|
||||||
/>
|
<div className="w-full grid sm:grid-cols-2">
|
||||||
<div className="py-2">{`${t('settled in')} ${
|
<div className="w-full grid sm:auto-rows-auto">
|
||||||
market.tradableInstrument.instrument.product.settlementAsset
|
<div className="font-extrabold py-2">{market.name}</div>
|
||||||
.symbol
|
<SimpleMarketExpires
|
||||||
}`}</div>
|
tags={market.tradableInstrument.instrument.metadata.tags}
|
||||||
</div>
|
|
||||||
<div className="w-full grid sm:grid-rows-2">
|
|
||||||
<div>
|
|
||||||
<SimpleMarketPercentChange
|
|
||||||
candles={market.candles}
|
|
||||||
marketId={market.id}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="py-2">{`${t('settled in')} ${
|
||||||
|
market.tradableInstrument.instrument.product
|
||||||
|
.settlementAsset.symbol
|
||||||
|
}`}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="w-full grid sm:grid-rows-2">
|
||||||
<Lozenge
|
<div>
|
||||||
variant={MARKET_STATUS[market.data?.market.state || '']}
|
<SimpleMarketPercentChange
|
||||||
>
|
candles={market.candles}
|
||||||
{market.data?.market.state}
|
marketId={market.id}
|
||||||
</Lozenge>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Lozenge
|
||||||
|
variant={MARKET_STATUS[market.data?.market.state || '']}
|
||||||
|
>
|
||||||
|
{market.data?.market.state}
|
||||||
|
</Lozenge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="absolute right-16 top-1/2 -translate-y-1/2">
|
||||||
<div className="absolute right-16 top-1/2 -translate-y-1/2">
|
<Button
|
||||||
<Button
|
onClick={() => onClick(market.id)}
|
||||||
onClick={() => onClick(market.id)}
|
variant="inline"
|
||||||
variant="inline"
|
prependIconName="chevron-right"
|
||||||
prependIconName="chevron-right"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
) : (
|
||||||
) : (
|
<Splash>{t('No data to display')}</Splash>
|
||||||
<Splash>{t('No data to display')}</Splash>
|
)}
|
||||||
)}
|
</AsyncRenderer>
|
||||||
</AsyncRenderer>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { MockedProvider } from '@apollo/react-testing';
|
||||||
|
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import type { MarketFilters } from './__generated__/MarketFilters';
|
||||||
|
import { FILTERS_QUERY } from './data-provider';
|
||||||
|
import filterData from './mocks/market-filters.json';
|
||||||
|
|
||||||
|
const mockedNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: () => mockedNavigate,
|
||||||
|
useParams: () => ({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SimpleMarketToolbar', () => {
|
||||||
|
const filterMock: MockedResponse<MarketFilters> = {
|
||||||
|
request: {
|
||||||
|
query: FILTERS_QUERY,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: filterData as unknown as MarketFilters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be properly rendered', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[filterMock]} addTypename={false}>
|
||||||
|
<SimpleMarketToolbar />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('market-products-menu').children).toHaveLength(3);
|
||||||
|
expect(screen.getByTestId('market-assets-menu').children).toHaveLength(6);
|
||||||
|
expect(screen.getByRole('combobox').children).toHaveLength(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigation should work well', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[filterMock]} addTypename={false}>
|
||||||
|
<SimpleMarketToolbar />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen
|
||||||
|
.getByTestId('market-products-menu')
|
||||||
|
.children[1].querySelector('button') as HTMLButtonElement
|
||||||
|
);
|
||||||
|
expect(mockedNavigate).toHaveBeenCalledWith('/markets/Active/all/Future');
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen
|
||||||
|
.getByTestId('market-assets-menu')
|
||||||
|
.children[5].querySelector('button') as HTMLButtonElement
|
||||||
|
);
|
||||||
|
expect(mockedNavigate).toHaveBeenCalledWith('/markets/Active/tEURO/Future');
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('combobox'), {
|
||||||
|
target: { value: 'Pending' },
|
||||||
|
});
|
||||||
|
expect(mockedNavigate).toHaveBeenCalledWith(
|
||||||
|
'/markets/Pending/tEURO/Future'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,151 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Button, Select } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import useMarketFiltersData from '../../hooks/use-markets-filter';
|
||||||
|
import { STATES_FILTER } from './constants';
|
||||||
|
|
||||||
|
const SimpleMarketToolbar = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const params = useParams();
|
||||||
|
const { assets, products, assetsPerProduct } = useMarketFiltersData();
|
||||||
|
const [activeNumber, setActiveNumber] = useState(
|
||||||
|
products?.length ? products.indexOf(params.product || '') + 1 : -1
|
||||||
|
);
|
||||||
|
const [activeAsset, setActiveAsset] = useState(params.asset || 'all');
|
||||||
|
const [activeState, setActiveState] = useState(params.state || 'Active');
|
||||||
|
const [sliderStyles, setSliderStyles] = useState<Record<string, string>>({});
|
||||||
|
const slideContRef = useRef<HTMLUListElement | null>(null);
|
||||||
|
|
||||||
|
const onStateChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setActiveState(e.target.value);
|
||||||
|
},
|
||||||
|
[setActiveState]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// handle corner case when there is product
|
||||||
|
// param, but no products yet
|
||||||
|
if (activeNumber < 0 && products?.length) {
|
||||||
|
setActiveNumber(products.indexOf(params.product || '') + 1 || 0);
|
||||||
|
}
|
||||||
|
}, [activeNumber, setActiveNumber, products, params]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const contStyles = (
|
||||||
|
slideContRef.current as HTMLUListElement
|
||||||
|
).getBoundingClientRect();
|
||||||
|
const selectedStyles = (slideContRef.current as HTMLUListElement).children[
|
||||||
|
activeNumber
|
||||||
|
]?.getBoundingClientRect();
|
||||||
|
const styles: Record<string, string> = selectedStyles
|
||||||
|
? {
|
||||||
|
backgroundColor: activeNumber ? '' : theme.colors.coral,
|
||||||
|
width: `${selectedStyles.width}px`,
|
||||||
|
left: `${selectedStyles.left - contStyles.left}px`,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
setSliderStyles(styles);
|
||||||
|
}, [activeNumber, slideContRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeNumber < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const product = activeNumber ? `/${products[activeNumber - 1]}` : '';
|
||||||
|
const asset = activeAsset !== 'all' || product ? `/${activeAsset}` : '';
|
||||||
|
const state = activeState !== 'Active' || asset ? `/${activeState}` : '';
|
||||||
|
navigate(`/markets${state}${asset}${product}`);
|
||||||
|
}, [activeNumber, activeAsset, activeState, products, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-max">
|
||||||
|
<ul
|
||||||
|
ref={slideContRef}
|
||||||
|
className="grid grid-flow-col auto-cols-min gap-8 relative pb-4"
|
||||||
|
data-testid="market-products-menu"
|
||||||
|
>
|
||||||
|
<li key="all" className="md:mx-16 whitespace-nowrap">
|
||||||
|
<Button
|
||||||
|
variant="inline"
|
||||||
|
onClick={() => setActiveNumber(0)}
|
||||||
|
style={{ color: theme.colors.coral }}
|
||||||
|
className={classNames({ active: !activeNumber })}
|
||||||
|
>
|
||||||
|
{t('All Markets')}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{products.map((product, i) => (
|
||||||
|
<li key={product} className="mx-16 whitespace-nowrap">
|
||||||
|
<Button
|
||||||
|
variant="inline"
|
||||||
|
onClick={() => setActiveNumber(++i)}
|
||||||
|
className={classNames({ active: activeNumber - 1 === i })}
|
||||||
|
>
|
||||||
|
{product}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className="absolute bottom-0 h-2 transition-left duration-300 dark:bg-white bg-black"
|
||||||
|
key="slider"
|
||||||
|
style={sliderStyles}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
<div className="grid gap-8 pb-4 mt-8 md:grid-cols-[min-content,min-content,1fr]">
|
||||||
|
<div className="md:ml-16">
|
||||||
|
<Select
|
||||||
|
value={activeState}
|
||||||
|
onChange={onStateChange}
|
||||||
|
name="states"
|
||||||
|
className="mr-16 w-auto"
|
||||||
|
>
|
||||||
|
{STATES_FILTER.map((state) => (
|
||||||
|
<option key={state.value} value={state.value}>
|
||||||
|
{state.text}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block">|</div>
|
||||||
|
<ul
|
||||||
|
className="grid grid-flow-col auto-cols-min md:gap-8 pb-4"
|
||||||
|
data-testid="market-assets-menu"
|
||||||
|
>
|
||||||
|
<li key="all" className="mx-8">
|
||||||
|
<Button
|
||||||
|
variant="inline"
|
||||||
|
onClick={() => setActiveAsset('all')}
|
||||||
|
className={classNames({
|
||||||
|
'font-bold': activeAsset === 'all',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('All')}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{(activeNumber
|
||||||
|
? assetsPerProduct[products[activeNumber - 1]]
|
||||||
|
: assets
|
||||||
|
)?.map((asset) => (
|
||||||
|
<li key={asset} className="mx-8">
|
||||||
|
<Button
|
||||||
|
variant="inline"
|
||||||
|
onClick={() => setActiveAsset(asset)}
|
||||||
|
className={classNames({
|
||||||
|
'font-bold': activeAsset === asset,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{asset}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SimpleMarketToolbar;
|
@ -0,0 +1,32 @@
|
|||||||
|
import type { SimpleMarkets_markets } from '../components/simple-market-list/__generated__/SimpleMarkets';
|
||||||
|
import type { RouterParams } from '../components/simple-market-list/simple-market-list';
|
||||||
|
|
||||||
|
const useMarketsFilterData = (
|
||||||
|
data: SimpleMarkets_markets[] = [],
|
||||||
|
params: RouterParams
|
||||||
|
) => {
|
||||||
|
return data.filter((item) => {
|
||||||
|
if (
|
||||||
|
params.product &&
|
||||||
|
params.product !== item.tradableInstrument.instrument.product.__typename
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
params.asset &&
|
||||||
|
params.asset !== 'all' &&
|
||||||
|
params.asset !==
|
||||||
|
item.tradableInstrument.instrument.product.settlementAsset.symbol
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const state =
|
||||||
|
params.state === 'all' ? '' : params.state ? params.state : 'Active';
|
||||||
|
if (state && state !== item.data?.market.state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMarketsFilterData;
|
45
apps/simple-trading-app/src/app/hooks/use-markets-filter.ts
Normal file
45
apps/simple-trading-app/src/app/hooks/use-markets-filter.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { FILTERS_QUERY } from '../components/simple-market-list/data-provider';
|
||||||
|
import type { MarketFilters } from '../components/simple-market-list/__generated__/MarketFilters';
|
||||||
|
|
||||||
|
const useMarketFilters = () => {
|
||||||
|
const [assets, setAssets] = useState<string[]>([]);
|
||||||
|
const [products, setProducts] = useState<string[]>([]);
|
||||||
|
const [assetsPerProduct, setAssetsPerProduct] = useState<
|
||||||
|
Record<string, string[]>
|
||||||
|
>({});
|
||||||
|
const { data } = useQuery<MarketFilters>(FILTERS_QUERY, {
|
||||||
|
pollInterval: 5000,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const localProducts = new Set<string>();
|
||||||
|
const localAssets = new Set<string>();
|
||||||
|
const localAssetPerProduct: Record<string, Set<string>> = {};
|
||||||
|
data?.markets?.forEach((item) => {
|
||||||
|
const product = item.tradableInstrument.instrument.product.__typename;
|
||||||
|
const asset =
|
||||||
|
item.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||||
|
if (!(product in localAssetPerProduct)) {
|
||||||
|
localAssetPerProduct[product] = new Set<string>();
|
||||||
|
}
|
||||||
|
localAssetPerProduct[product].add(asset);
|
||||||
|
localProducts.add(product);
|
||||||
|
localAssets.add(asset);
|
||||||
|
});
|
||||||
|
setAssets([...localAssets]);
|
||||||
|
setProducts([...localProducts]);
|
||||||
|
setAssetsPerProduct(
|
||||||
|
Object.entries(localAssetPerProduct).reduce(
|
||||||
|
(agg: Record<string, string[]>, entry) => {
|
||||||
|
agg[entry[0]] = [...entry[1]];
|
||||||
|
return agg;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
return { assets, products, assetsPerProduct };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMarketFilters;
|
@ -20,6 +20,19 @@ export const routerConfig = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.MARKETS,
|
path: ROUTES.MARKETS,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: `:state`,
|
||||||
|
element: <SimpleMarketList />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: `:asset`,
|
||||||
|
element: <SimpleMarketList />,
|
||||||
|
children: [{ path: `:product`, element: <SimpleMarketList /> }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
name: 'Markets',
|
name: 'Markets',
|
||||||
text: t('Markets'),
|
text: t('Markets'),
|
||||||
element: <SimpleMarketList />,
|
element: <SimpleMarketList />,
|
||||||
|
Loading…
Reference in New Issue
Block a user