diff --git a/libs/markets/src/lib/components/markets-container/market-list-table.tsx b/libs/markets/src/lib/components/markets-container/market-list-table.tsx index 3f780ff4c..ae4aa792f 100644 --- a/libs/markets/src/lib/components/markets-container/market-list-table.tsx +++ b/libs/markets/src/lib/components/markets-container/market-list-table.tsx @@ -1,29 +1,14 @@ import { forwardRef } from 'react'; -import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; -import { t } from '@vegaprotocol/i18n'; -import type { - VegaValueGetterParams, - VegaValueFormatterParams, - VegaICellRendererParams, - TypedDataAgGrid, -} from '@vegaprotocol/datagrid'; -import { COL_DEFS } from '@vegaprotocol/datagrid'; +import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid, PriceFlashCell, MarketNameCell, - SetFilter, } from '@vegaprotocol/datagrid'; -import { ButtonLink } from '@vegaprotocol/ui-toolkit'; -import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react'; -import * as Schema from '@vegaprotocol/types'; import type { MarketMaybeWithData } from '../../markets-provider'; -import { MarketTableActions } from './market-table-actions'; import { OracleStatus } from './oracle-status'; -import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; - -const { MarketTradingMode, AuctionTrigger } = Schema; +import { useColumnDefs } from './use-column-defs'; export const getRowId = ({ data }: { data: { id: string } }) => data.id; @@ -51,199 +36,34 @@ const MarketName = (props: MarketNameCellProps) => ( ); +const defaultColDef = { + resizable: true, + sortable: true, + filter: true, + filterParams: { buttons: ['reset'] }, + minWidth: 100, +}; + export const MarketListTable = forwardRef< AgGridReact, TypedDataAgGrid & { onMarketClick: (marketId: string, metaKey?: boolean) => void; } >(({ onMarketClick, ...props }, ref) => { - const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + const columnDefs = useColumnDefs({ onMarketClick }); + return ( - - - ) => { - if (!data?.data) return undefined; - const { trigger, marketTradingMode } = data.data; - return marketTradingMode === - MarketTradingMode.TRADING_MODE_MONITORING_AUCTION && - trigger && - trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED - ? `${Schema.MarketTradingModeMapping[marketTradingMode]} - - ${Schema.AuctionTriggerMapping[trigger]}` - : Schema.MarketTradingModeMapping[marketTradingMode]; - }} - filter={SetFilter} - filterParams={{ - set: Schema.MarketTradingModeMapping, - }} - /> - ) => { - return data?.state ? Schema.MarketStateMapping[data.state] : '-'; - }} - filter={SetFilter} - filterParams={{ - set: Schema.MarketStateMapping, - }} - /> - ) => { - return data?.data?.bestBidPrice === undefined - ? undefined - : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams< - MarketMaybeWithData, - 'data.bestBidPrice' - >) => - data?.data?.bestBidPrice === undefined - ? undefined - : addDecimalsFormatNumber( - data.data.bestBidPrice, - data.decimalPlaces - ) - } - /> - ) => { - return data?.data?.bestOfferPrice === undefined - ? undefined - : toBigNum( - data?.data?.bestOfferPrice, - data.decimalPlaces - ).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams< - MarketMaybeWithData, - 'data.bestOfferPrice' - >) => - data?.data?.bestOfferPrice === undefined - ? undefined - : addDecimalsFormatNumber( - data.data.bestOfferPrice, - data.decimalPlaces - ) - } - /> - ) => { - return data?.data?.markPrice === undefined - ? undefined - : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => - data?.data?.bestOfferPrice === undefined - ? undefined - : addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces) - } - /> - ) => { - const value = - data?.tradableInstrument.instrument.product.settlementAsset; - return value ? ( - { - openAssetDetailsDialog(value.id, e.target as HTMLElement); - }} - > - {value.symbol} - - ) : ( - '' - ); - }} - /> - ) => { - if (!data) return null; - return ( - - ); - }} - /> - + /> ); }); diff --git a/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx b/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx new file mode 100644 index 000000000..ce1ee5f5d --- /dev/null +++ b/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx @@ -0,0 +1,107 @@ +import { render, screen, act, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as DataProviders from '@vegaprotocol/data-provider'; +import { MockedProvider } from '@apollo/react-testing'; +import type { MarketMaybeWithData } from '../../markets-provider'; +import { MarketsContainer } from './markets-container'; + +const market = { + id: 'id-1', + tradableInstrument: { + instrument: { + product: { settlementAsset: { id: 'assetId-1' } }, + }, + }, + decimalPlaces: 1, + positionDecimalPlaces: 1, + state: 'STATE_ACTIVE', + tradingMode: 'TRADING_MODE_OPENING_AUCTION', + data: { + bestBidPrice: 100, + }, +} as unknown as MarketMaybeWithData; + +describe('MarketsContainer', () => { + it('context menu should stay open', async () => { + const spyOnSelect = jest.fn(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(DataProviders, 'useDataProvider') + .mockImplementation(() => { + return { + error: null, + reload: jest.fn(), + data: [market], + }; + }); + + let rerenderRef: (ui: React.ReactElement) => void; + await act(async () => { + const { rerender } = render( + + + + ); + rerenderRef = rerender; + }); + + // make sure ag grid is finished initializaing + const rowContainer = await screen.findByRole('rowgroup', { + name: (_name, element) => + element.classList.contains('ag-center-cols-container'), + }); + expect(within(rowContainer).getAllByRole('row')).toHaveLength(1); + expect( + screen.getByRole('rowgroup', { + name: (_name, element) => + element.classList.contains('ag-pinned-right-cols-container'), + }) + ).toBeInTheDocument(); + + // open the dropdown + await userEvent.click( + screen.getByRole('button', { + name: (_name, element) => + (element.parentNode as Element)?.getAttribute('id') === + 'cell-market-actions-8', + }) + ); + + await checkDropdown(); + + // reset the mock and rerender so the component + // updates with new data + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(DataProviders, 'useDataProvider') + .mockImplementation(() => { + return { + error: null, + reload: jest.fn(), + data: [{ ...market, state: 'STATE_PENDING' }], + }; + }); + + // @ts-ignore we await the act above so rerenderRef is definitely defined + rerenderRef( + + + + ); + + // make sure dropdown is still open + await checkDropdown(); + + async function checkDropdown() { + const dropdownContent = await screen.findByTestId( + 'market-actions-content' + ); + expect(dropdownContent).toBeInTheDocument(); + expect( + within(dropdownContent).getByRole('menuitem', { + name: 'Copy Market ID', + }) + ).toBeInTheDocument(); + } + }); +}); diff --git a/libs/markets/src/lib/components/markets-container/use-column-defs.tsx b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx new file mode 100644 index 000000000..0bd0a4bee --- /dev/null +++ b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx @@ -0,0 +1,196 @@ +import { useMemo } from 'react'; +import type { ColDef } from 'ag-grid-community'; +import { t } from '@vegaprotocol/i18n'; +import type { + VegaICellRendererParams, + VegaValueFormatterParams, + VegaValueGetterParams, +} from '@vegaprotocol/datagrid'; +import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid'; +import * as Schema from '@vegaprotocol/types'; +import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; +import { ButtonLink } from '@vegaprotocol/ui-toolkit'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; +import type { MarketMaybeWithData } from '../../markets-provider'; +import { MarketTableActions } from './market-table-actions'; + +interface Props { + onMarketClick: (marketId: string, metaKey?: boolean) => void; +} + +const { MarketTradingMode, AuctionTrigger } = Schema; + +export const useColumnDefs = ({ onMarketClick }: Props) => { + const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + + return useMemo( + () => [ + { + headerName: t('Market'), + field: 'tradableInstrument.instrument.code', + cellRenderer: 'MarketName', + cellRendererParams: { onMarketClick }, + }, + { + headerName: t('Description'), + field: 'tradableInstrument.instrument.name', + }, + { + headerName: t('Trading mode'), + field: 'tradingMode', + minWidth: 170, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + if (!data?.data) return '-'; + const { trigger, marketTradingMode } = data.data; + return marketTradingMode === + MarketTradingMode.TRADING_MODE_MONITORING_AUCTION && + trigger && + trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED + ? `${Schema.MarketTradingModeMapping[marketTradingMode]} + - ${Schema.AuctionTriggerMapping[trigger]}` + : Schema.MarketTradingModeMapping[marketTradingMode]; + }, + filter: SetFilter, + filterParams: { + set: Schema.MarketTradingModeMapping, + }, + }, + { + headerName: t('Status'), + field: 'state', + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + return data?.state ? Schema.MarketStateMapping[data.state] : '-'; + }, + filter: SetFilter, + filterParams: { + set: Schema.MarketStateMapping, + }, + }, + { + headerName: t('Best bid'), + field: 'data.bestBidPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams) => { + return data?.data?.bestBidPrice === undefined + ? undefined + : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + MarketMaybeWithData, + 'data.bestBidPrice' + >) => + data?.data?.bestBidPrice === undefined + ? '-' + : addDecimalsFormatNumber( + data.data.bestBidPrice, + data.decimalPlaces + ), + }, + { + headerName: t('Best offer'), + field: 'data.bestOfferPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams< + MarketMaybeWithData, + 'data.bestOfferPrice' + >) => { + return data?.data?.bestOfferPrice === undefined + ? undefined + : toBigNum( + data?.data?.bestOfferPrice, + data.decimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + MarketMaybeWithData, + 'data.bestOfferPrice' + >) => + data?.data?.bestOfferPrice === undefined + ? '-' + : addDecimalsFormatNumber( + data.data.bestOfferPrice, + data.decimalPlaces + ), + }, + { + headerName: t('Mark price'), + field: 'data.markPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams) => { + return data?.data?.markPrice === undefined + ? undefined + : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => + data?.data?.bestOfferPrice === undefined + ? '-' + : addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces), + }, + { + headerName: t('Settlement asset'), + field: 'tradableInstrument.instrument.product.settlementAsset.symbol', + cellRenderer: ({ + data, + }: VegaICellRendererParams< + MarketMaybeWithData, + 'tradableInstrument.instrument.product.settlementAsset.symbol' + >) => { + const value = + data?.tradableInstrument.instrument.product.settlementAsset; + return value ? ( + { + openAssetDetailsDialog(value.id, e.target as HTMLElement); + }} + > + {value.symbol} + + ) : ( + '' + ); + }, + }, + { + colId: 'market-actions', + field: 'id', + ...COL_DEFS.actions, + cellRenderer: ({ + data, + }: VegaICellRendererParams) => { + if (!data) return null; + return ( + + ); + }, + }, + ], + [onMarketClick, openAssetDetailsDialog] + ); +};