feat(trading): distinguish between product types (#4543)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Maciek 2023-08-16 16:43:23 +02:00 committed by GitHub
parent e3ed71bec4
commit 9fcf286e4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 507 additions and 88 deletions

View File

@ -256,6 +256,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
cy.get(rowSelector)
.first()
.find('[col-id="code"]')
.find('[data-testid="market-code"]')
.should('have.text', settledMarket.tradableInstrument.instrument.code);
// 6001-MARK-002

View File

@ -4,7 +4,8 @@ import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
const colInstrumentCode =
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {

View File

@ -40,21 +40,25 @@ describe('markets selector', { tags: '@smoke' }, () => {
code: 'SOLUSD',
markPrice: '84.41',
vol: '0.00',
productType: 'Futr',
},
{
code: 'ETHBTC.QM21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'BTCUSD.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'AAPL.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
];
cy.getByTestId('header-title').should('be.visible').click();
@ -64,7 +68,9 @@ describe('markets selector', { tags: '@smoke' }, () => {
const market = data[i];
// 6001-MARK-021
// 6001-MARK-022
expect(item.find('h3').text()).equals(market.code);
expect(item.find('h3').text()).equals(
`${market.code} ${market.productType}`
);
expect(
item.find('[data-testid="market-selector-volume"]').text()
).contains(market.vol);

View File

@ -3,7 +3,7 @@ import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"]';
const colMarketId = '[col-id="market"] [data-testid="market-code"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
@ -155,7 +155,13 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting('market', marketColDefault, marketColAsc, marketColDesc);
checkSorting(
'market',
marketColDefault,
marketColAsc,
marketColDesc,
' [data-testid="market-code"]'
);
const stateColDefault = [
'Open',

View File

@ -120,7 +120,9 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(`.ag-center-cols-container [col-id='${orderSymbol}']`)
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];

View File

@ -166,7 +166,8 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
marketsSortedDesc,
' [data-testid="market-code"]'
);
});

View File

@ -214,7 +214,6 @@ describe('Closed', () => {
</MemoryRouter>
);
});
// screen.debug(document, Infinity);
const headers = screen.getAllByRole('columnheader');
const expectedHeaders = [
@ -434,7 +433,7 @@ describe('Closed', () => {
await waitFor(() => {
expect(
screen.getByRole('button', { name: 'SuccessorCode' })
screen.getByRole('button', { name: /^SuccessorCode/ })
).toBeInTheDocument();
});
expect(
@ -443,6 +442,16 @@ describe('Closed', () => {
element.getAttribute('col-id') === 'successorMarket',
})
).toBeInTheDocument();
screen
.getAllByRole('gridcell', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
.forEach((element) => {
expect(element.querySelector('[title="Future"]')?.textContent).toEqual(
'Futr'
);
});
});
it('feature flag should hide successors', async () => {

View File

@ -4,7 +4,11 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
@ -47,6 +51,7 @@ interface Row {
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
tradingTerminationOracleId: string;
settlementAsset: SettlementAsset;
productType: string;
}
export const Closed = () => {
@ -90,6 +95,7 @@ export const Closed = () => {
tradingTerminationOracleId:
instrument.product.dataSourceSpecForTradingTermination.id,
settlementAsset: instrument.product.settlementAsset,
productType: instrument.product.__typename || '',
};
return row;
@ -115,16 +121,7 @@ const ClosedMarketsDataGrid = ({
{
headerName: t('Market'),
field: 'code',
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<Row, 'code'>) => {
return (
<span data-testid="market-code" data-market-id={data?.id}>
{value}
</span>
);
},
cellRenderer: 'MarketNameCell',
},
{
headerName: t('Description'),
@ -279,7 +276,7 @@ const ClosedMarketsDataGrid = ({
rowData={rowData}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
components={{ SuccessorMarketRenderer }}
components={{ SuccessorMarketRenderer, MarketNameCell }}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
/>
);

View File

@ -0,0 +1,190 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import userEvent from '@testing-library/user-event';
import type { Market } from '@vegaprotocol/markets';
import { SuccessorMarketRenderer } from './successor-market-cell';
import {
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import { createMarketFragment } from '@vegaprotocol/mock';
const mockSuccessorsQuery = [
{
id: 'market1',
parentMarketID: 'parentMarket1',
successorMarketID: 'successorMarket1',
},
{ id: 'market2', parentMarketID: 'parentMarket2' },
{ id: 'market3', successorMarketID: 'successorMarket3' },
];
const parentMarket1 = {
id: 'parentMarket1',
tradableInstrument: {
instrument: { code: 'code parent 1', id: '1' },
},
} as unknown as Market;
const successorMarket1 = {
id: 'successorMarket1',
tradableInstrument: {
instrument: { code: 'code successor 1', id: '2' },
},
} as unknown as Market;
const parentMarket2 = {
id: 'parentMarket2',
tradableInstrument: {
instrument: { code: 'code parent 2', id: '3' },
},
} as unknown as Market;
const successorMarket3 = {
id: 'successorMarket3',
tradableInstrument: {
instrument: { code: 'code successor 3', id: '4' },
},
} as unknown as Market;
const mockMarkets = [
parentMarket1,
successorMarket1,
parentMarket2,
successorMarket3,
];
const mockClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: jest.fn().mockImplementation(() => mockClickHandler),
}));
const marketMock = {
request: {
query: MarketsDocument,
variables: undefined,
},
result: {
data: {
marketsConnection: {
edges: mockMarkets.map((item) => ({
node: {
...createMarketFragment(item),
},
})),
},
},
},
};
const successorMock = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
edges: mockSuccessorsQuery.map((item) => ({
node: {
...item,
},
})),
},
},
},
};
const mocks = [marketMock, successorMock];
describe('SuccessorMarketRenderer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should properly rendered successor market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket1', false);
});
});
it('should properly rendered parent market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket1', false);
});
});
it('should properly rendered only parent market', async () => {
const successorValue = 'market2';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 2')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket2', false);
});
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should properly rendered only successor market', async () => {
const successorValue = 'market3';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 3')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket3', false);
});
await act(() => {
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
});
expect(screen.getByText('-')).toBeInTheDocument();
});
});

View File

@ -1,8 +1,8 @@
import React from 'react';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import React from 'react';
export const SuccessorMarketRenderer = ({
value,
@ -33,6 +33,7 @@ export const SuccessorMarketRenderer = ({
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
productType={data.tradableInstrument.instrument?.product.__typename}
/>
) : (
'-'

View File

@ -99,6 +99,7 @@ describe('MarketSelectorItem', () => {
currentMarketId={market.id}
style={{}}
onSelect={jest.fn()}
allProducts
/>
</MockedProvider>
</MemoryRouter>
@ -181,5 +182,7 @@ describe('MarketSelectorItem', () => {
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
});
expect(screen.getByText('Futr')).toBeInTheDocument();
});
});

View File

@ -12,17 +12,20 @@ import {
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { MarketProductPill } from '@vegaprotocol/datagrid';
export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
allProducts,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect: (marketId: string) => void;
allProducts: boolean;
}) => {
return (
<div style={style} role="row">
@ -36,13 +39,19 @@ export const MarketSelectorItem = ({
})}
onClick={() => onSelect(market.id)}
>
<MarketData market={market} />
<MarketData market={market} allProducts={allProducts} />
</Link>
</div>
);
};
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const MarketData = ({
market,
allProducts,
}: {
market: MarketMaybeWithDataAndCandles;
allProducts: boolean;
}) => {
const { data } = useMarketDataUpdateSubscription({
variables: {
marketId: market.id,
@ -84,7 +93,14 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
<>
<div className="w-2/5" role="gridcell">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}
{market.tradableInstrument.instrument.code}{' '}
{allProducts && (
<MarketProductPill
productType={
market.tradableInstrument.instrument.product.__typename
}
/>
)}
</h3>
{mode && (
<p className="text-xs text-vega-orange-500 dark:text-vega-orange-550 whitespace-nowrap">

View File

@ -43,7 +43,7 @@ export const MarketSelector = ({
sort: Sort.None,
assets: [],
});
const allProducts = filter.product === Product.All;
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
@ -128,6 +128,7 @@ export const MarketSelector = ({
? t('Spot markets coming soon.')
: t('No markets')
}
allProducts={allProducts}
/>
</div>
</div>
@ -141,6 +142,7 @@ const MarketList = ({
currentMarketId,
onSelect,
noItems,
allProducts,
}: {
data: MarketMaybeWithDataAndCandles[];
error: Error | undefined;
@ -149,6 +151,7 @@ const MarketList = ({
currentMarketId?: string;
onSelect: (marketId: string) => void;
noItems: string;
allProducts: boolean;
}) => {
const itemSize = 45;
const listRef = useRef<HTMLDivElement | null>(null);
@ -192,6 +195,7 @@ const MarketList = ({
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
allProducts={allProducts}
/>
</div>
</TinyScroll>
@ -202,6 +206,7 @@ interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect: (marketId: string) => void;
currentMarketId?: string;
allProducts: boolean;
}
const ListItem = ({
@ -218,6 +223,7 @@ const ListItem = ({
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
allProducts={data.allProducts}
/>
);
@ -229,19 +235,21 @@ const List = ({
onSelect,
noItems,
currentMarketId,
allProducts,
}: ListItemData & {
loading: boolean;
height: number;
itemSize: number;
noItems: string;
allProducts: boolean;
}) => {
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
() => ({ data, onSelect, currentMarketId }),
[data, onSelect, currentMarketId]
() => ({ data, onSelect, currentMarketId, allProducts }),
[data, onSelect, currentMarketId, allProducts]
);
if (!data || loading) {
return (

View File

@ -24,6 +24,9 @@ const singleRow = {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
product: {
__typename: 'Future',
},
},
},
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
@ -120,6 +123,9 @@ describe('BreakdownTable', () => {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
product: {
__typename: 'Future',
},
},
},
},

View File

@ -36,14 +36,24 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
valueFormatter: ({
minWidth: 200,
cellRenderer: ({
value,
}: VegaValueFormatterParams<
data,
}: VegaICellRendererParams<
AccountFields,
'market.tradableInstrument.instrument.code'
>) => {
if (!value) return 'None';
return value;
return value ? (
<MarketNameCell
value={value}
productType={
data?.market?.tradableInstrument.instrument.product.__typename
}
/>
) : (
'None'
);
},
},
{
@ -126,7 +136,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
}
ref={ref}
rowHeight={34}
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
components={{ PriceCell, ProgressBarCell }}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
columnDefs={coldefs}

View File

@ -41,21 +41,22 @@ export const checkSorting = (
column: string,
orderTabDefault: string[],
orderTabAsc: string[],
orderTabDesc: string[]
orderTabDesc: string[],
additionalColumnSelector = ''
) => {
checkSortChange(orderTabDefault, column);
checkSortChange(orderTabDefault, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).last().click();
});
checkSortChange(orderTabAsc, column);
checkSortChange(orderTabAsc, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
checkSortChange(orderTabDesc, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
@ -63,13 +64,20 @@ export const checkSorting = (
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
const checkSortChange = (
tabsArr: string[],
column: string,
additionalColumnSelector = ''
) => {
cy.get('.ag-center-cols-container')
.last()
.within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
cy.get(`[col-id="${column}"]${additionalColumnSelector}`).should(
'have.text',
tabsArr[i]
);
});
});
});

View File

@ -9,7 +9,10 @@ export const CenteredGridCellWrapper = ({
className?: string;
}) => (
<div
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
className={classNames(
'flex h-[20px] p-0 justify-items-center items-center',
className
)}
>
<div className="w-full self-center">{children}</div>
</div>

View File

@ -1,13 +1,39 @@
import type { MouseEvent, ReactNode } from 'react';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import get from 'lodash/get';
import { Pill } from '@vegaprotocol/ui-toolkit';
import type { Market } from '@vegaprotocol/types';
const productTypeMap = {
Future: 'Futr',
FutureProduct: 'Futr',
Spot: 'Spot',
SpotProduct: 'Spot',
Perpetual: 'Perp',
PerpetualProduct: 'Perp',
} as const;
export type ProductType = keyof typeof productTypeMap | undefined;
export const MarketProductPill = ({
productType,
}: {
productType?: ProductType;
}) => {
return productType ? (
<Pill size="xxs" className="uppercase ml-0.5" title={productType}>
{productTypeMap[productType] || productType}
</Pill>
) : null;
};
interface MarketNameCellProps {
value?: string | null;
data?: { id?: string; marketId?: string; market?: { id: string } };
data?:
| { id?: string; marketId?: string; productType?: string; market?: Market }
| Market;
idPath?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
defaultValue?: ReactNode;
productType?: ProductType;
}
export const MarketNameCell = ({
@ -15,29 +41,43 @@ export const MarketNameCell = ({
data,
idPath,
onMarketClick,
productType,
}: MarketNameCellProps) => {
const id = data ? get(data, idPath ?? 'id', 'all') : '';
const handleOnClick = useCallback(
(ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
if (onMarketClick) {
onMarketClick(id, ev.metaKey || ev.ctrlKey);
}
id && onMarketClick?.(id, ev.metaKey || ev.ctrlKey);
},
[id, onMarketClick]
);
if (!value || !data) return null;
return onMarketClick ? (
productType =
productType ||
(data as { productType?: ProductType }).productType ||
(data as Market)?.tradableInstrument?.instrument.product.__typename ||
(data as { market: Market })?.market?.tradableInstrument.instrument.product
.__typename;
if (!value) return;
const content = (
<>
<span data-testid="market-code" data-market-id={id}>
{value}
</span>
<MarketProductPill productType={productType} />
</>
);
return onMarketClick && id ? (
<button
onClick={handleOnClick}
tabIndex={0}
className="block text-left text-ellipsis overflow-hidden whitespace-nowrap w-full"
>
{value}
{content}
</button>
) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{value}</>
content
);
};

View File

@ -60,7 +60,7 @@ export const ledgerEntriesProvider = makeDerivedDataProvider<
(partsData) => {
const entries = partsData[0] as ReturnType<typeof getData>;
const assets = partsData[1] as Record<string, Asset>;
const markets = partsData[1] as Record<string, Market>;
const markets = partsData[2] as Record<string, Market>;
return entries.map((entry) => {
const asset = entry.assetId
? (assets as Record<string, Asset>)[entry.assetId]

View File

@ -12,10 +12,12 @@ import type {
import {
AgGridLazy as AgGrid,
DateRangeFilter,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import type { Market } from '@vegaprotocol/markets';
import {
AccountTypeMapping,
DescriptionTransferTypeMapping,
@ -82,10 +84,14 @@ export const LedgerTable = (props: LedgerEntryProps) => {
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
>) =>
value && (
<MarketNameCell value={value} data={data?.marketSender as Market} />
),
},
{
headerName: t('Receiver'),
@ -112,10 +118,17 @@ export const LedgerTable = (props: LedgerEntryProps) => {
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
>) =>
value && (
<MarketNameCell
value={value}
data={data?.marketReceiver as Market}
/>
),
},
{
headerName: t('Transfer type'),

View File

@ -51,6 +51,7 @@ export interface Position {
totalBalance: string;
unrealisedPNL: string;
updatedAt: string | null;
productType?: string;
}
export const getMetrics = (
@ -135,6 +136,7 @@ export const getMetrics = (
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL,
updatedAt: position.updatedAt || null,
productType: market?.tradableInstrument.instrument.product.__typename,
});
});
return metrics;

View File

@ -37,6 +37,7 @@ const singleRow: Position = {
totalBalance: '123456',
unrealisedPNL: '456',
updatedAt: '2022-07-27T15:02:58.400Z',
productType: 'Future',
};
const singleRowData = [singleRow];
@ -80,6 +81,7 @@ describe('Positions', () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
expect(screen.getByText('Futr')).toBeInTheDocument();
});
it('Does not fail if the market name does not match the split pattern', async () => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import { useColumnDefs } from './use-column-defs';
import { MarketNameProposalCell, useColumnDefs } from './use-column-defs';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
@ -43,7 +43,7 @@ export const ProposalsList = ({
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer }}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
</div>
);

View File

@ -5,6 +5,7 @@ import {
CenteredGridCellWrapper,
COL_DEFS,
DateRangeFilter,
MarketProductPill,
SetFilter,
} from '@vegaprotocol/datagrid';
import compact from 'lodash/compact';
@ -20,13 +21,42 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { InstrumentConfiguration } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
import { getMarketProductType } from '../../utils/get-market-product-type';
export const MarketNameProposalCell = ({
value,
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
const productType = getMarketProductType(
change.instrument as InstrumentConfiguration
);
const content = (
<>
<span data-testid="market-code">{value as string}</span>
<MarketProductPill productType={productType} />
</>
);
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return <ExternalLink href={link}>{content}</ExternalLink>;
}
return content;
}
return null;
};
export const useColumnDefs = () => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
@ -43,26 +73,7 @@ export const useColumnDefs = () => {
headerName: t('Market'),
field: 'terms.change.instrument.code',
cellStyle: { lineHeight: '14px' },
cellRenderer: ({
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return (
<ExternalLink href={link}>
{change.instrument.code}
</ExternalLink>
);
}
return change.instrument.code;
}
return null;
},
cellRenderer: 'MarketNameProposalCell',
},
{
colId: 'description',
@ -155,7 +166,7 @@ export const useColumnDefs = () => {
},
},
]);
}, [VEGA_TOKEN_URL, requiredMajorityPercentage]);
}, [requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => {
return {

View File

@ -0,0 +1,58 @@
import type { InstrumentConfiguration } from '@vegaprotocol/types';
import { getMarketProductType } from './get-market-product-type';
describe('getMarketProductType', () => {
it('should resolve product type properly', () => {
expect(
getMarketProductType({
futureProduct: {
quoteName: 'Market 1',
},
} as InstrumentConfiguration)
).toEqual('Future');
expect(
getMarketProductType({
spotProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Spot');
expect(
getMarketProductType({
perpetualProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Perpetual');
expect(
getMarketProductType({
product: {
__typename: 'Perpetual',
},
futureProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Perpetual');
expect(
getMarketProductType({
product: {
__typename: 'Spot',
},
futureProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Spot');
expect(
getMarketProductType({
product: {
__typename: 'Future',
},
perpetualProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Future');
});
});

View File

@ -0,0 +1,16 @@
import type { InstrumentConfiguration, Product } from '@vegaprotocol/types';
// it needs to be adjusted after deploy this https://github.com/vegaprotocol/vega/pull/9003
export const getMarketProductType = (
instrumentConfiguration: InstrumentConfiguration
) => {
return 'product' in instrumentConfiguration
? (instrumentConfiguration.product as Product).__typename
: 'futureProduct' in instrumentConfiguration
? 'Future'
: 'spotProduct' in instrumentConfiguration
? 'Spot'
: 'perpetualProduct' in instrumentConfiguration
? 'Perpetual'
: undefined;
};

View File

@ -1,18 +1,18 @@
import type { ReactNode } from 'react';
import type { ReactNode, HTMLProps } from 'react';
import { Intent } from '../../utils/intent';
import classNames from 'classnames';
type Size = 'lg' | 'md' | 'sm' | 'xs' | 'xxs';
interface Props {
interface Props extends Omit<HTMLProps<HTMLSpanElement>, 'size'> {
children: ReactNode;
intent?: Intent;
size?: Size;
className?: string;
}
const getClasses = (size: Size, intent?: Intent, className?: string) => {
const getClasses = (size: Size, intent: Intent, className?: string) => {
return classNames(
['rounded-md', 'leading-none', 'font-alpha', 'py-1 px-2'],
['rounded-md', 'leading-none', 'font-alpha'],
{
'bg-vega-yellow dark:bg-vega-yellow': intent === Intent.Primary,
'bg-vega-clight-500 dark:bg-vega-cdark-500': intent === Intent.None,
@ -25,19 +25,28 @@ const getClasses = (size: Size, intent?: Intent, className?: string) => {
intent === Intent.Primary,
},
{
'text-lg': size === 'lg',
'text-base': size === 'md',
'text-sma': size === 'sm',
'text-xs': size === 'xs',
'text-[10px]': size === 'xxs',
'text-lg py-1 px-2': size === 'lg',
'text-base py-1 px-2': size === 'md',
'text-sm py-1 px-1': size === 'sm',
'text-xs py-1 px-1': size === 'xs',
'text-[10px] py-0 px-1': size === 'xxs',
},
className
);
};
export const Pill = ({ intent, size, className, children }: Props) => {
export const Pill = ({
intent,
size,
className,
children,
...props
}: Props) => {
return (
<span className={getClasses(size || 'md', intent, className)}>
<span
className={getClasses(size || 'md', intent || Intent.None, className)}
{...props}
>
{children}
</span>
);