diff --git a/apps/trading-e2e/src/integration/closed-markets.cy.ts b/apps/trading-e2e/src/integration/closed-markets.cy.ts index 677519d0f..f6d621ed7 100644 --- a/apps/trading-e2e/src/integration/closed-markets.cy.ts +++ b/apps/trading-e2e/src/integration/closed-markets.cy.ts @@ -363,12 +363,11 @@ describe('Closed markets', { tags: '@smoke' }, () => { .first() .find('button svg') .should('exist'); - if (Cypress.env('NX_SUCCESSOR_MARKETS')) { cy.get(rowSelector) - .find('[col-id="successorMarketID"]') + .find('[col-id="successorMarket"]') .first() - .should('have.text', ' - '); + .should('have.text', '-'); } }); diff --git a/apps/trading-e2e/src/integration/market-all.cy.ts b/apps/trading-e2e/src/integration/market-all.cy.ts index 95c78d497..ff4d1aab1 100644 --- a/apps/trading-e2e/src/integration/market-all.cy.ts +++ b/apps/trading-e2e/src/integration/market-all.cy.ts @@ -27,6 +27,7 @@ describe('markets all table', { tags: '@smoke' }, () => { 'Description', 'Trading mode', 'Status', + 'Successor market', 'Best bid', 'Best offer', 'Mark price', diff --git a/apps/trading-e2e/src/integration/markets-proposed.cy.ts b/apps/trading-e2e/src/integration/markets-proposed.cy.ts index 1cb30314c..0d51d505a 100644 --- a/apps/trading-e2e/src/integration/markets-proposed.cy.ts +++ b/apps/trading-e2e/src/integration/markets-proposed.cy.ts @@ -19,6 +19,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => { 'Description', 'Settlement asset', 'State', + 'Parent market', 'Voting', 'Closing date', 'Enactment date', diff --git a/apps/trading/client-pages/markets/closed.spec.tsx b/apps/trading/client-pages/markets/closed.spec.tsx index c92afcac0..4d6aa2bed 100644 --- a/apps/trading/client-pages/markets/closed.spec.tsx +++ b/apps/trading/client-pages/markets/closed.spec.tsx @@ -10,15 +10,18 @@ import type { OracleSpecDataConnectionQuery, MarketsDataQuery, MarketsQuery, + SuccessorMarketIdsQuery, } from '@vegaprotocol/markets'; import { OracleSpecDataConnectionDocument, MarketsDataDocument, MarketsDocument, + SuccessorMarketIdsDocument, } from '@vegaprotocol/markets'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import { FLAGS } from '@vegaprotocol/environment'; import { createMarketFragment, marketsQuery, @@ -51,6 +54,17 @@ jest.mock('@vegaprotocol/environment', () => ({ FLAGS: { SUCCESSOR_MARKETS: true } as Partial, })); +jest.mock('@vegaprotocol/environment', () => { + const actual = jest.requireActual('@vegaprotocol/environment'); + return { + ...actual, + FLAGS: { + ...actual.FLAGS, + SUCCESSOR_MARKETS: true, + }, + }; +}); + describe('Closed', () => { let originalNow: typeof Date.now; const mockNowTimestamp = 1672531200000; @@ -349,8 +363,25 @@ describe('Closed', () => { state: MarketState.STATE_SETTLED, }), }, + { + __typename: 'MarketEdge' as const, + node: { + ...createMarketFragment({ + id: 'successorMarketID', + state: MarketState.STATE_ACTIVE, + }), + tradableInstrument: { + ...createMarketFragment().tradableInstrument, + instrument: { + ...createMarketFragment().tradableInstrument.instrument, + id: 'successorAssset', + name: 'Successor Market Name', + code: 'SuccessorCode', + }, + }, + }, + }, ]; - const mixedMarketsMock: MockedResponse = { request: { query: MarketsDocument, @@ -364,11 +395,36 @@ describe('Closed', () => { }, }, }; - + const successorMarketsMock: MockedResponse = { + request: { + query: SuccessorMarketIdsDocument, + }, + result: { + data: { + marketsConnection: { + __typename: 'MarketConnection', + edges: [ + { + node: { + id: 'include-0', + successorMarketID: 'successorMarketID', + parentMarketID: '', + }, + }, + ], + }, + }, + }, + }; render( { screen.getByRole('button', { name: 'SuccessorCode' }) ).toBeInTheDocument(); }); + expect( + screen.getByRole('columnheader', { + name: (_name, element) => + element.getAttribute('col-id') === 'successorMarket', + }) + ).toBeInTheDocument(); + }); + + it('feature flag should hide successors', async () => { + const mockedFlags = jest.mocked(FLAGS); + mockedFlags.SUCCESSOR_MARKETS = false; + + const mixedMarkets = [ + { + __typename: 'MarketEdge' as const, + node: createMarketFragment({ + id: 'include-0', + state: MarketState.STATE_SETTLED, + }), + }, + { + __typename: 'MarketEdge' as const, + node: { + ...createMarketFragment({ + id: 'successorMarketID', + state: MarketState.STATE_ACTIVE, + }), + tradableInstrument: { + ...createMarketFragment().tradableInstrument, + instrument: { + ...createMarketFragment().tradableInstrument.instrument, + id: 'successorAssset', + name: 'Successor Market Name', + code: 'SuccessorCode', + }, + }, + }, + }, + ]; + const mixedMarketsMock: MockedResponse = { + request: { + query: MarketsDocument, + }, + result: { + data: { + marketsConnection: { + __typename: 'MarketConnection', + edges: mixedMarkets, + }, + }, + }, + }; + const successorMarketsMock: MockedResponse = { + request: { + query: SuccessorMarketIdsDocument, + }, + result: { + data: { + marketsConnection: { + __typename: 'MarketConnection', + edges: [ + { + node: { + id: 'include-0', + successorMarketID: 'successorMarketID', + parentMarketID: '', + }, + }, + ], + }, + }, + }, + }; + render( + + + + + + + + ); + await waitFor(() => { + expect( + screen.getByRole('columnheader', { + name: (_name, element) => + element.getAttribute('col-id') === 'settlementDate', + }) + ).toBeInTheDocument(); + }); + screen.getAllByRole('columnheader').forEach((element) => { + expect(element.getAttribute('col-id')).not.toEqual('successorMarket'); + }); }); }); diff --git a/apps/trading/client-pages/markets/closed.tsx b/apps/trading/client-pages/markets/closed.tsx index 6d6ed2e5a..2b0603dea 100644 --- a/apps/trading/client-pages/markets/closed.tsx +++ b/apps/trading/client-pages/markets/closed.tsx @@ -4,11 +4,7 @@ import type { VegaICellRendererParams, VegaValueFormatterParams, } from '@vegaprotocol/datagrid'; -import { - AgGridLazy as AgGrid, - COL_DEFS, - MarketNameCell, -} from '@vegaprotocol/datagrid'; +import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid'; import { useMemo } from 'react'; import { t } from '@vegaprotocol/i18n'; import { MarketState, MarketStateMapping } from '@vegaprotocol/types'; @@ -23,15 +19,14 @@ import type { import { MarketActionsDropdown, closedMarketsWithDataProvider, - useSuccessorMarket, } from '@vegaprotocol/markets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import type { ColDef } from 'ag-grid-community'; +import { FLAGS } from '@vegaprotocol/environment'; import { SettlementDateCell } from './settlement-date-cell'; import { SettlementPriceCell } from './settlement-price-cell'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; -import { FLAGS } from '@vegaprotocol/environment'; +import { SuccessorMarketRenderer } from './successor-market-cell'; type SettlementAsset = MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset']; @@ -106,22 +101,6 @@ export const Closed = () => { ); }; -export const SuccessorMarketRenderer = ({ - value, -}: VegaICellRendererParams) => { - const { data } = useSuccessorMarket(value); - const onMarketClick = useMarketClickHandler(); - return data ? ( - - ) : ( - ' - ' - ); -}; - const ClosedMarketsDataGrid = ({ rowData, error, @@ -202,8 +181,8 @@ const ClosedMarketsDataGrid = ({ }, FLAGS.SUCCESSOR_MARKETS && { headerName: t('Successor market'), - colId: 'successorMarketID', field: 'id', + colId: 'successorMarket', cellRenderer: 'SuccessorMarketRenderer', }, { diff --git a/apps/trading/client-pages/markets/markets.tsx b/apps/trading/client-pages/markets/markets.tsx index 7033ce286..057e906a0 100644 --- a/apps/trading/client-pages/markets/markets.tsx +++ b/apps/trading/client-pages/markets/markets.tsx @@ -1,7 +1,13 @@ import { MarketsContainer } from '@vegaprotocol/markets'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; +import { SuccessorMarketRenderer } from './successor-market-cell'; export const Markets = () => { const handleOnSelect = useMarketClickHandler(); - return ; + return ( + + ); }; diff --git a/apps/trading/client-pages/markets/proposed.tsx b/apps/trading/client-pages/markets/proposed.tsx index 25f5a5252..009190e17 100644 --- a/apps/trading/client-pages/markets/proposed.tsx +++ b/apps/trading/client-pages/markets/proposed.tsx @@ -6,6 +6,7 @@ import { } from '@vegaprotocol/environment'; import { ProposalsList } from '@vegaprotocol/proposals'; import { ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { SuccessorMarketRenderer } from './successor-market-cell'; export const Proposed = () => { const tokenLink = useLinks(DApp.Token); @@ -13,7 +14,7 @@ export const Proposed = () => { return ( <>
- +
{t('Propose a new market')} diff --git a/apps/trading/client-pages/markets/successor-market-cell.tsx b/apps/trading/client-pages/markets/successor-market-cell.tsx new file mode 100644 index 000000000..378e28472 --- /dev/null +++ b/apps/trading/client-pages/markets/successor-market-cell.tsx @@ -0,0 +1,40 @@ +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, + parent, +}: { + value: string; + parent?: boolean; +}) => { + const successors = useSuccessorMarketIds(value); + const onMarketClick = useMarketClickHandler(); + + const lookupValue = successors + ? parent + ? successors.parentMarketID + : successors.successorMarketID + : ''; + + const { data } = useDataProvider({ + dataProvider: marketProvider, + variables: { + marketId: lookupValue || '', + }, + skip: !lookupValue, + }); + + return data ? ( + + ) : ( + '-' + ); +}; diff --git a/libs/markets/src/lib/SuccessorMarket.graphql b/libs/markets/src/lib/SuccessorMarket.graphql index fc3f3f007..4f5255987 100644 --- a/libs/markets/src/lib/SuccessorMarket.graphql +++ b/libs/markets/src/lib/SuccessorMarket.graphql @@ -16,6 +16,7 @@ query SuccessorMarketIds { node { id successorMarketID + parentMarketID } } } diff --git a/libs/markets/src/lib/__generated__/SuccessorMarket.ts b/libs/markets/src/lib/__generated__/SuccessorMarket.ts index 03bd413e5..d41c58fe1 100644 --- a/libs/markets/src/lib/__generated__/SuccessorMarket.ts +++ b/libs/markets/src/lib/__generated__/SuccessorMarket.ts @@ -20,7 +20,7 @@ export type ParentMarketIdQuery = { __typename?: 'Query', market?: { __typename? export type SuccessorMarketIdsQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type SuccessorMarketIdsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, successorMarketID?: string | null } }> } | null }; +export type SuccessorMarketIdsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, successorMarketID?: string | null, parentMarketID?: string | null } }> } | null }; export type SuccessorMarketQueryVariables = Types.Exact<{ marketId: Types.Scalars['ID']; @@ -107,6 +107,7 @@ export const SuccessorMarketIdsDocument = gql` node { id successorMarketID + parentMarketID } } } @@ -182,4 +183,4 @@ export function useSuccessorMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOp } export type SuccessorMarketQueryHookResult = ReturnType; export type SuccessorMarketLazyQueryHookResult = ReturnType; -export type SuccessorMarketQueryResult = Apollo.QueryResult; \ No newline at end of file +export type SuccessorMarketQueryResult = Apollo.QueryResult; 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 a8a2aa9c0..f56b03801 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,4 +1,4 @@ -import { forwardRef } from 'react'; +import React, { forwardRef } from 'react'; import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid, @@ -48,10 +48,15 @@ export const MarketListTable = forwardRef< AgGridReact, TypedDataAgGrid & { onMarketClick: (marketId: string, metaKey?: boolean) => void; + SuccessorMarketRenderer?: React.FC<{ value: string }>; } ->(({ onMarketClick, ...props }, ref) => { +>(({ onMarketClick, SuccessorMarketRenderer, ...props }, ref) => { const columnDefs = useColumnDefs({ onMarketClick }); - + const components = { + PriceFlashCell, + MarketName, + ...(SuccessorMarketRenderer ? { SuccessorMarketRenderer } : 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 index ce1ee5f5d..34e2d74e0 100644 --- a/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx +++ b/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx @@ -4,6 +4,21 @@ import * as DataProviders from '@vegaprotocol/data-provider'; import { MockedProvider } from '@apollo/react-testing'; import type { MarketMaybeWithData } from '../../markets-provider'; import { MarketsContainer } from './markets-container'; +import { FLAGS } from '@vegaprotocol/environment'; + +jest.mock('@vegaprotocol/environment', () => { + const actual = jest.requireActual('@vegaprotocol/environment'); + return { + ...actual, + FLAGS: { + ...actual.FLAGS, + SUCCESSOR_MARKETS: true, + }, + }; +}); +const SuccessorMarketRenderer = ({ value }: { value: string }) => { + return '-'; +}; const market = { id: 'id-1', @@ -22,8 +37,10 @@ const market = { } as unknown as MarketMaybeWithData; describe('MarketsContainer', () => { - it('context menu should stay open', async () => { - const spyOnSelect = jest.fn(); + const spyOnSelect = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + jest // eslint-disable-next-line @typescript-eslint/no-explicit-any .spyOn(DataProviders, 'useDataProvider') @@ -34,12 +51,16 @@ describe('MarketsContainer', () => { data: [market], }; }); - + }); + it('context menu should stay open', async () => { let rerenderRef: (ui: React.ReactElement) => void; await act(async () => { const { rerender } = render( - + ); rerenderRef = rerender; @@ -63,7 +84,7 @@ describe('MarketsContainer', () => { screen.getByRole('button', { name: (_name, element) => (element.parentNode as Element)?.getAttribute('id') === - 'cell-market-actions-8', + 'cell-market-actions-9', }) ); @@ -104,4 +125,55 @@ describe('MarketsContainer', () => { ).toBeInTheDocument(); } }); + + it('SuccessorMarketRenderer should be rendered', async () => { + const successorMarketName = 'Successor Market Name'; + const spySuccessorMarketRenderer = jest + .fn() + .mockReturnValue(successorMarketName); + + render( + + + + ); + + expect(spySuccessorMarketRenderer).toHaveBeenCalled(); + expect( + screen.getByRole('columnheader', { + name: (_name, element) => + element.getAttribute('col-id') === 'successorMarketID', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('presentation', { + name: (_name, element) => + element.getAttribute('id') === 'cell-successorMarketID-14', + }) + ).toHaveTextContent(successorMarketName); + }); + + it('feature flag should hide successorMarketID column', async () => { + const mockedFlags = jest.mocked(FLAGS); + mockedFlags.SUCCESSOR_MARKETS = false; + + const spySuccessorMarketRenderer = jest.fn(); + + render( + + + + ); + + expect(spySuccessorMarketRenderer).not.toHaveBeenCalled(); + screen.getAllByRole('columnheader').forEach((element) => { + expect(element.getAttribute('col-id')).not.toEqual('successorMarketID'); + }); + }); }); diff --git a/libs/markets/src/lib/components/markets-container/markets-container.tsx b/libs/markets/src/lib/components/markets-container/markets-container.tsx index 76a05c5b0..f4e140dde 100644 --- a/libs/markets/src/lib/components/markets-container/markets-container.tsx +++ b/libs/markets/src/lib/components/markets-container/markets-container.tsx @@ -1,5 +1,5 @@ import type { MouseEvent } from 'react'; -import { useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import type { AgGridReact } from 'ag-grid-react'; import type { CellClickedEvent } from 'ag-grid-community'; import { t } from '@vegaprotocol/i18n'; @@ -11,9 +11,13 @@ import type { MarketMaybeWithData } from '../../markets-provider'; const POLLING_TIME = 2000; interface MarketsContainerProps { onSelect: (marketId: string, metaKey?: boolean) => void; + SuccessorMarketRenderer?: React.FC<{ value: string }>; } -export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { +export const MarketsContainer = ({ + onSelect, + SuccessorMarketRenderer, +}: MarketsContainerProps) => { const gridRef = useRef(null); const { data, error, reload } = useDataProvider({ @@ -58,6 +62,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { }} onMarketClick={onSelect} overlayNoRowsTemplate={error ? error.message : t('No markets')} + SuccessorMarketRenderer={SuccessorMarketRenderer} /> ); 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 index 0c032b5c2..f924487d7 100644 --- a/libs/markets/src/lib/components/markets-container/use-column-defs.tsx +++ b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import type { ColDef } from 'ag-grid-community'; +import compact from 'lodash/compact'; import { t } from '@vegaprotocol/i18n'; import type { VegaICellRendererParams, @@ -11,6 +12,7 @@ import * as Schema from '@vegaprotocol/types'; import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; +import { FLAGS } from '@vegaprotocol/environment'; import type { MarketMaybeWithData } from '../../markets-provider'; import { MarketActionsDropdown } from './market-table-actions'; @@ -22,166 +24,183 @@ 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]} + () => + compact([ + { + 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]; + : Schema.MarketTradingModeMapping[marketTradingMode]; + }, + filter: SetFilter, + filterParams: { + set: Schema.MarketTradingModeMapping, + }, }, - 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('Status'), - field: 'state', - valueFormatter: ({ - data, - }: VegaValueFormatterParams) => { - return data?.state ? Schema.MarketStateMapping[data.state] : '-'; + FLAGS.SUCCESSOR_MARKETS && { + headerName: t('Successor market'), + field: 'successorMarketID', + cellRenderer: 'SuccessorMarketRenderer', }, - 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 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(); + { + headerName: t('Best offer'), + field: 'data.bestOfferPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams) => { + 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 + ), }, - 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) => { - return data?.data?.bestOfferPrice === undefined - ? undefined - : toBigNum( - data?.data?.bestOfferPrice, - data.decimalPlaces - ).toNumber(); + { + 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 + ), }, - 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(); + { + 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} + + ) : ( + '' + ); + }, }, - 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 ( + + ); + }, }, - }, - { - colId: 'market-actions', - field: 'id', - ...COL_DEFS.actions, - cellRenderer: ({ - data, - }: VegaICellRendererParams) => { - if (!data) return null; - return ( - - ); - }, - }, - ], + ]), [onMarketClick, openAssetDetailsDialog] ); }; diff --git a/libs/markets/src/lib/markets-provider.ts b/libs/markets/src/lib/markets-provider.ts index 18a0a235f..70cd3b76c 100644 --- a/libs/markets/src/lib/markets-provider.ts +++ b/libs/markets/src/lib/markets-provider.ts @@ -27,8 +27,9 @@ import { filterAndSortMarkets, } from './market-utils'; import { MarketsDocument } from './__generated__/markets'; - import type { Candle } from './market-candles-provider'; +import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket'; +import { SuccessorMarketIdsDocument } from './__generated__'; export type Market = MarketFieldsFragment; @@ -240,3 +241,34 @@ export const useMarketList = () => { reload, }; }; + +export type MarketSuccessors = { + __typename?: 'Market'; + id: string; + successorMarketID?: string | null; + parentMarketID?: string | null; +}; +const getMarketSuccessorData = ( + responseData: SuccessorMarketIdsQuery | null +): MarketSuccessors[] | null => + responseData?.marketsConnection?.edges.map((edge) => edge.node) || null; + +export const marketSuccessorProvider = makeDataProvider< + SuccessorMarketIdsQuery, + MarketSuccessors[], + never, + never +>({ + query: SuccessorMarketIdsDocument, + getData: getMarketSuccessorData, + fetchPolicy: 'no-cache', +}); + +export const useSuccessorMarketIds = (marketId: string) => { + const { data } = useDataProvider({ + dataProvider: marketSuccessorProvider, + variables: undefined, + skip: !marketId, + }); + return data?.find((item) => item.id === marketId) ?? null; +}; diff --git a/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx b/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx index 0a5d5052e..dc003b0e9 100644 --- a/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx +++ b/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx @@ -14,6 +14,23 @@ import { createProposalListFieldsFragment } from '../../lib/proposals-data-provi import type { ProposalsListQuery } from '../../lib'; import { ProposalsListDocument } from '../../lib'; import type { PartialDeep } from 'type-fest'; +import { FLAGS } from '@vegaprotocol/environment'; + +jest.mock('@vegaprotocol/environment', () => { + const actual = jest.requireActual('@vegaprotocol/environment'); + return { + ...actual, + FLAGS: { + ...actual.FLAGS, + SUCCESSOR_MARKETS: true, + }, + }; +}); + +const successorMarketName = 'Successor Market Name'; +const spySuccessorMarketRenderer = jest + .fn() + .mockReturnValue(successorMarketName); describe('ProposalsList', () => { const createProposalsMock = (override?: PartialDeep) => { @@ -64,13 +81,15 @@ describe('ProposalsList', () => { return mock; }; - + beforeEach(() => { + jest.clearAllMocks(); + }); it('should be properly rendered', async () => { const mock = createProposalsMock(); await act(() => { render( - + ); }); @@ -82,31 +101,55 @@ describe('ProposalsList', () => { }); it('some of states should be filtered out', async () => { + const proposalNode = createProposalListFieldsFragment({ + id: 'id-1', + state: Types.ProposalState.STATE_ENACTED, + }); + const mock = createProposalsMock({ proposalsConnection: { edges: [ { __typename: 'ProposalEdge', - node: createProposalListFieldsFragment({ - id: 'id-1', - state: Types.ProposalState.STATE_ENACTED, - }), + node: { + ...proposalNode, + terms: { + ...proposalNode.terms, + change: { + ...proposalNode.terms.change, + }, + }, + }, }, ], }, - }); + } as PartialDeep); await act(() => { render( - + ); }); const container = document.querySelector('.ag-center-cols-container'); await waitFor(() => { expect(container).toBeInTheDocument(); + expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2); }); - expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2); + + expect(spySuccessorMarketRenderer).toHaveBeenCalled(); + expect( + screen.getByRole('columnheader', { + name: (_name, element) => + element.getAttribute('col-id') === 'parentMarket', + }) + ).toBeInTheDocument(); + expect( + screen.getAllByRole('gridcell', { + name: (name, element) => + element.getAttribute('col-id') === 'parentMarket', + })[0] + ).toHaveTextContent(successorMarketName); }); it('empty response should causes no data message display', async () => { @@ -129,10 +172,55 @@ describe('ProposalsList', () => { await act(() => { render( - + ); }); expect(await screen.findByText('No markets')).toBeInTheDocument(); + + expect( + screen.getByRole('columnheader', { + name: (_name, element) => + element.getAttribute('col-id') === 'parentMarket', + }) + ).toBeInTheDocument(); + }); + + it('feature flag should hide parent marketcolumn', async () => { + const mockedFlags = jest.mocked(FLAGS); + mockedFlags.SUCCESSOR_MARKETS = false; + const mock: MockedResponse = { + request: { + query: ProposalsListDocument, + variables: { + proposalType: Types.ProposalType.TYPE_NEW_MARKET, + }, + }, + result: { + data: { + proposalsConnection: { + __typename: 'ProposalsConnection', + edges: [], + }, + }, + }, + }; + await act(() => { + render( + + + + ); + }); + await waitFor(() => { + expect( + screen.getByRole('columnheader', { + name: (_name, element) => element.getAttribute('col-id') === 'market', + }) + ).toBeInTheDocument(); + }); + screen.getAllByRole('columnheader').forEach((element) => { + expect(element.getAttribute('col-id')).not.toEqual('parentMarket'); + }); }); }); diff --git a/libs/proposals/src/components/proposals-list/proposals-list.tsx b/libs/proposals/src/components/proposals-list/proposals-list.tsx index 13409df6d..90fca13d3 100644 --- a/libs/proposals/src/components/proposals-list/proposals-list.tsx +++ b/libs/proposals/src/components/proposals-list/proposals-list.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { useRef } from 'react'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { t } from '@vegaprotocol/i18n'; @@ -17,7 +18,13 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) => ].includes(proposal.state) ); -export const ProposalsList = () => { +interface ProposalListProps { + SuccessorMarketRenderer: React.FC<{ value: string }>; +} + +export const ProposalsList = ({ + SuccessorMarketRenderer, +}: ProposalListProps) => { const gridRef = useRef(null); const { data } = useProposalsListQuery({ variables: { @@ -41,6 +48,7 @@ export const ProposalsList = () => { getRowId={({ data }) => data.id} style={{ width: '100%', height: '100%' }} overlayNoRowsTemplate={t('No markets')} + components={{ SuccessorMarketRenderer }} /> ); diff --git a/libs/proposals/src/components/proposals-list/use-column-defs.tsx b/libs/proposals/src/components/proposals-list/use-column-defs.tsx index 1b86e7ebd..33e08b2f3 100644 --- a/libs/proposals/src/components/proposals-list/use-column-defs.tsx +++ b/libs/proposals/src/components/proposals-list/use-column-defs.tsx @@ -2,7 +2,8 @@ import { useMemo } from 'react'; import BigNumber from 'bignumber.js'; import type { ColDef } from 'ag-grid-community'; import { COL_DEFS, DateRangeFilter, SetFilter } from '@vegaprotocol/datagrid'; -import { useEnvironment } from '@vegaprotocol/environment'; +import compact from 'lodash/compact'; +import { useEnvironment, FLAGS } from '@vegaprotocol/environment'; import { getDateTimeFormat } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { @@ -32,7 +33,7 @@ export const useColumnDefs = () => { const cellCss = 'grid h-full items-center'; const columnDefs: ColDef[] = useMemo(() => { - return [ + return compact([ { colId: 'market', headerName: t('Market'), @@ -83,6 +84,13 @@ export const useColumnDefs = () => { set: ProposalStateMapping, }, }, + FLAGS.SUCCESSOR_MARKETS && { + headerName: t('Parent market'), + field: 'id', + colId: 'parentMarket', + cellRenderer: 'SuccessorMarketRenderer', + cellRendererParams: { parent: true }, + }, { colId: 'voting', headerName: t('Voting'), @@ -146,7 +154,7 @@ export const useColumnDefs = () => { }, flex: 1, }, - ]; + ]); }, [VEGA_TOKEN_URL, requiredMajorityPercentage]); const defaultColDef: ColDef = useMemo(() => {