chore(trading): market relationship - parent market in proposals list (#4339)

Co-authored-by: asiaznik <artur@vegaprotocol.io>
Co-authored-by: Edd <edd@vega.xyz>
This commit is contained in:
Maciek 2023-07-25 17:32:40 +02:00 committed by GitHub
parent caa0076396
commit d28db485c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 635 additions and 211 deletions

View File

@ -363,12 +363,11 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.first() .first()
.find('button svg') .find('button svg')
.should('exist'); .should('exist');
if (Cypress.env('NX_SUCCESSOR_MARKETS')) { if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
cy.get(rowSelector) cy.get(rowSelector)
.find('[col-id="successorMarketID"]') .find('[col-id="successorMarket"]')
.first() .first()
.should('have.text', ' - '); .should('have.text', '-');
} }
}); });

View File

@ -27,6 +27,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'Description', 'Description',
'Trading mode', 'Trading mode',
'Status', 'Status',
'Successor market',
'Best bid', 'Best bid',
'Best offer', 'Best offer',
'Mark price', 'Mark price',

View File

@ -19,6 +19,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'Description', 'Description',
'Settlement asset', 'Settlement asset',
'State', 'State',
'Parent market',
'Voting', 'Voting',
'Closing date', 'Closing date',
'Enactment date', 'Enactment date',

View File

@ -10,15 +10,18 @@ import type {
OracleSpecDataConnectionQuery, OracleSpecDataConnectionQuery,
MarketsDataQuery, MarketsDataQuery,
MarketsQuery, MarketsQuery,
SuccessorMarketIdsQuery,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { import {
OracleSpecDataConnectionDocument, OracleSpecDataConnectionDocument,
MarketsDataDocument, MarketsDataDocument,
MarketsDocument, MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
import { import {
createMarketFragment, createMarketFragment,
marketsQuery, marketsQuery,
@ -51,6 +54,17 @@ jest.mock('@vegaprotocol/environment', () => ({
FLAGS: { SUCCESSOR_MARKETS: true } as Partial<FeatureFlags>, FLAGS: { SUCCESSOR_MARKETS: true } as Partial<FeatureFlags>,
})); }));
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
},
};
});
describe('Closed', () => { describe('Closed', () => {
let originalNow: typeof Date.now; let originalNow: typeof Date.now;
const mockNowTimestamp = 1672531200000; const mockNowTimestamp = 1672531200000;
@ -349,8 +363,25 @@ describe('Closed', () => {
state: MarketState.STATE_SETTLED, 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<MarketsQuery> = { const mixedMarketsMock: MockedResponse<MarketsQuery> = {
request: { request: {
query: MarketsDocument, query: MarketsDocument,
@ -364,11 +395,36 @@ describe('Closed', () => {
}, },
}, },
}; };
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
node: {
id: 'include-0',
successorMarketID: 'successorMarketID',
parentMarketID: '',
},
},
],
},
},
},
};
render( render(
<MemoryRouter> <MemoryRouter>
<MockedProvider <MockedProvider
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]} mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
> >
<VegaWalletContext.Provider <VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape} value={{ pubKey } as VegaWalletContextShape}
@ -384,5 +440,107 @@ describe('Closed', () => {
screen.getByRole('button', { name: 'SuccessorCode' }) screen.getByRole('button', { name: 'SuccessorCode' })
).toBeInTheDocument(); ).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<MarketsQuery> = {
request: {
query: MarketsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: mixedMarkets,
},
},
},
};
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
node: {
id: 'include-0',
successorMarketID: 'successorMarketID',
parentMarketID: '',
},
},
],
},
},
},
};
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
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');
});
}); });
}); });

View File

@ -4,11 +4,7 @@ import type {
VegaICellRendererParams, VegaICellRendererParams,
VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
AgGridLazy as AgGrid,
COL_DEFS,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types'; import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
@ -23,15 +19,14 @@ import type {
import { import {
MarketActionsDropdown, MarketActionsDropdown,
closedMarketsWithDataProvider, closedMarketsWithDataProvider,
useSuccessorMarket,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import { FLAGS } from '@vegaprotocol/environment';
import { SettlementDateCell } from './settlement-date-cell'; import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell'; import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { SuccessorMarketRenderer } from './successor-market-cell';
import { FLAGS } from '@vegaprotocol/environment';
type SettlementAsset = type SettlementAsset =
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset']; MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
@ -106,22 +101,6 @@ export const Closed = () => {
); );
}; };
export const SuccessorMarketRenderer = ({
value,
}: VegaICellRendererParams<Row, 'id'>) => {
const { data } = useSuccessorMarket(value);
const onMarketClick = useMarketClickHandler();
return data ? (
<MarketNameCell
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
/>
) : (
' - '
);
};
const ClosedMarketsDataGrid = ({ const ClosedMarketsDataGrid = ({
rowData, rowData,
error, error,
@ -202,8 +181,8 @@ const ClosedMarketsDataGrid = ({
}, },
FLAGS.SUCCESSOR_MARKETS && { FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Successor market'), headerName: t('Successor market'),
colId: 'successorMarketID',
field: 'id', field: 'id',
colId: 'successorMarket',
cellRenderer: 'SuccessorMarketRenderer', cellRenderer: 'SuccessorMarketRenderer',
}, },
{ {

View File

@ -1,7 +1,13 @@
import { MarketsContainer } from '@vegaprotocol/markets'; import { MarketsContainer } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Markets = () => { export const Markets = () => {
const handleOnSelect = useMarketClickHandler(); const handleOnSelect = useMarketClickHandler();
return <MarketsContainer onSelect={handleOnSelect} />; return (
<MarketsContainer
onSelect={handleOnSelect}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
);
}; };

View File

@ -6,6 +6,7 @@ import {
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { ProposalsList } from '@vegaprotocol/proposals'; import { ProposalsList } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Proposed = () => { export const Proposed = () => {
const tokenLink = useLinks(DApp.Token); const tokenLink = useLinks(DApp.Token);
@ -13,7 +14,7 @@ export const Proposed = () => {
return ( return (
<> <>
<div className="h-[400px]"> <div className="h-[400px]">
<ProposalsList /> <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />
</div> </div>
<ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}> <ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}>
{t('Propose a new market')} {t('Propose a new market')}

View File

@ -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 ? (
<MarketNameCell
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
/>
) : (
'-'
);
};

View File

@ -16,6 +16,7 @@ query SuccessorMarketIds {
node { node {
id id
successorMarketID successorMarketID
parentMarketID
} }
} }
} }

View File

@ -20,7 +20,7 @@ export type ParentMarketIdQuery = { __typename?: 'Query', market?: { __typename?
export type SuccessorMarketIdsQueryVariables = Types.Exact<{ [key: string]: never; }>; 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<{ export type SuccessorMarketQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
@ -107,6 +107,7 @@ export const SuccessorMarketIdsDocument = gql`
node { node {
id id
successorMarketID successorMarketID
parentMarketID
} }
} }
} }
@ -182,4 +183,4 @@ export function useSuccessorMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOp
} }
export type SuccessorMarketQueryHookResult = ReturnType<typeof useSuccessorMarketQuery>; export type SuccessorMarketQueryHookResult = ReturnType<typeof useSuccessorMarketQuery>;
export type SuccessorMarketLazyQueryHookResult = ReturnType<typeof useSuccessorMarketLazyQuery>; export type SuccessorMarketLazyQueryHookResult = ReturnType<typeof useSuccessorMarketLazyQuery>;
export type SuccessorMarketQueryResult = Apollo.QueryResult<SuccessorMarketQuery, SuccessorMarketQueryVariables>; export type SuccessorMarketQueryResult = Apollo.QueryResult<SuccessorMarketQuery, SuccessorMarketQueryVariables>;

View File

@ -1,4 +1,4 @@
import { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { import {
AgGridLazy as AgGrid, AgGridLazy as AgGrid,
@ -48,10 +48,15 @@ export const MarketListTable = forwardRef<
AgGridReact, AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & { TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void; onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
} }
>(({ onMarketClick, ...props }, ref) => { >(({ onMarketClick, SuccessorMarketRenderer, ...props }, ref) => {
const columnDefs = useColumnDefs({ onMarketClick }); const columnDefs = useColumnDefs({ onMarketClick });
const components = {
PriceFlashCell,
MarketName,
...(SuccessorMarketRenderer ? { SuccessorMarketRenderer } : null),
};
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
@ -60,7 +65,7 @@ export const MarketListTable = forwardRef<
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
columnDefs={columnDefs} columnDefs={columnDefs}
suppressCellFocus suppressCellFocus
components={{ PriceFlashCell, MarketName }} components={components}
{...props} {...props}
/> />
); );

View File

@ -4,6 +4,21 @@ import * as DataProviders from '@vegaprotocol/data-provider';
import { MockedProvider } from '@apollo/react-testing'; import { MockedProvider } from '@apollo/react-testing';
import type { MarketMaybeWithData } from '../../markets-provider'; import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketsContainer } from './markets-container'; 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 = { const market = {
id: 'id-1', id: 'id-1',
@ -22,8 +37,10 @@ const market = {
} as unknown as MarketMaybeWithData; } as unknown as MarketMaybeWithData;
describe('MarketsContainer', () => { describe('MarketsContainer', () => {
it('context menu should stay open', async () => { const spyOnSelect = jest.fn();
const spyOnSelect = jest.fn(); beforeEach(() => {
jest.clearAllMocks();
jest jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider') .spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
@ -34,12 +51,16 @@ describe('MarketsContainer', () => {
data: [market], data: [market],
}; };
}); });
});
it('context menu should stay open', async () => {
let rerenderRef: (ui: React.ReactElement) => void; let rerenderRef: (ui: React.ReactElement) => void;
await act(async () => { await act(async () => {
const { rerender } = render( const { rerender } = render(
<MockedProvider> <MockedProvider>
<MarketsContainer onSelect={spyOnSelect} /> <MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
</MockedProvider> </MockedProvider>
); );
rerenderRef = rerender; rerenderRef = rerender;
@ -63,7 +84,7 @@ describe('MarketsContainer', () => {
screen.getByRole('button', { screen.getByRole('button', {
name: (_name, element) => name: (_name, element) =>
(element.parentNode as Element)?.getAttribute('id') === (element.parentNode as Element)?.getAttribute('id') ===
'cell-market-actions-8', 'cell-market-actions-9',
}) })
); );
@ -104,4 +125,55 @@ describe('MarketsContainer', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
} }
}); });
it('SuccessorMarketRenderer should be rendered', async () => {
const successorMarketName = 'Successor Market Name';
const spySuccessorMarketRenderer = jest
.fn()
.mockReturnValue(successorMarketName);
render(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={spySuccessorMarketRenderer}
/>
</MockedProvider>
);
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(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={spySuccessorMarketRenderer}
/>
</MockedProvider>
);
expect(spySuccessorMarketRenderer).not.toHaveBeenCalled();
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('successorMarketID');
});
});
}); });

View File

@ -1,5 +1,5 @@
import type { MouseEvent } from 'react'; 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 { AgGridReact } from 'ag-grid-react';
import type { CellClickedEvent } from 'ag-grid-community'; import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@ -11,9 +11,13 @@ import type { MarketMaybeWithData } from '../../markets-provider';
const POLLING_TIME = 2000; const POLLING_TIME = 2000;
interface MarketsContainerProps { interface MarketsContainerProps {
onSelect: (marketId: string, metaKey?: boolean) => void; 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<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const { data, error, reload } = useDataProvider({ const { data, error, reload } = useDataProvider({
@ -58,6 +62,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
}} }}
onMarketClick={onSelect} onMarketClick={onSelect}
overlayNoRowsTemplate={error ? error.message : t('No markets')} overlayNoRowsTemplate={error ? error.message : t('No markets')}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/> />
</div> </div>
); );

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import compact from 'lodash/compact';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { import type {
VegaICellRendererParams, VegaICellRendererParams,
@ -11,6 +12,7 @@ import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { FLAGS } from '@vegaprotocol/environment';
import type { MarketMaybeWithData } from '../../markets-provider'; import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
@ -22,166 +24,183 @@ const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = ({ onMarketClick }: Props) => { export const useColumnDefs = ({ onMarketClick }: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>( return useMemo<ColDef[]>(
() => [ () =>
{ compact([
headerName: t('Market'), {
field: 'tradableInstrument.instrument.code', headerName: t('Market'),
cellRenderer: 'MarketName', field: 'tradableInstrument.instrument.code',
cellRendererParams: { onMarketClick }, cellRenderer: 'MarketName',
}, cellRendererParams: { onMarketClick },
{ },
headerName: t('Description'), {
field: 'tradableInstrument.instrument.name', headerName: t('Description'),
}, field: 'tradableInstrument.instrument.name',
{ },
headerName: t('Trading mode'), {
field: 'tradingMode', headerName: t('Trading mode'),
minWidth: 170, field: 'tradingMode',
valueFormatter: ({ minWidth: 170,
data, valueFormatter: ({
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => { data,
if (!data?.data) return '-'; }: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
const { trigger, marketTradingMode } = data.data; if (!data?.data) return '-';
return marketTradingMode === const { trigger, marketTradingMode } = data.data;
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION && return marketTradingMode ===
trigger && MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED trigger &&
? `${Schema.MarketTradingModeMapping[marketTradingMode]} trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}` - ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode]; : Schema.MarketTradingModeMapping[marketTradingMode];
},
filter: SetFilter,
filterParams: {
set: Schema.MarketTradingModeMapping,
},
}, },
filter: SetFilter, {
filterParams: { headerName: t('Status'),
set: Schema.MarketTradingModeMapping, field: 'state',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
},
filter: SetFilter,
filterParams: {
set: Schema.MarketStateMapping,
},
}, },
}, FLAGS.SUCCESSOR_MARKETS && {
{ headerName: t('Successor market'),
headerName: t('Status'), field: 'successorMarketID',
field: 'state', cellRenderer: 'SuccessorMarketRenderer',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}, },
filter: SetFilter, {
filterParams: { headerName: t('Best bid'),
set: Schema.MarketStateMapping, field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
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'),
headerName: t('Best bid'), field: 'data.bestOfferPrice',
field: 'data.bestBidPrice', type: 'rightAligned',
type: 'rightAligned', cellRenderer: 'PriceFlashCell',
cellRenderer: 'PriceFlashCell', filter: 'agNumberColumnFilter',
filter: 'agNumberColumnFilter', valueGetter: ({
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => { data,
return data?.data?.bestBidPrice === undefined }: VegaValueGetterParams<MarketMaybeWithData>) => {
? undefined return data?.data?.bestOfferPrice === undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); ? 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, headerName: t('Mark price'),
}: VegaValueFormatterParams< field: 'data.markPrice',
MarketMaybeWithData, type: 'rightAligned',
'data.bestBidPrice' cellRenderer: 'PriceFlashCell',
>) => filter: 'agNumberColumnFilter',
data?.data?.bestBidPrice === undefined valueGetter: ({
? '-' data,
: addDecimalsFormatNumber( }: VegaValueGetterParams<MarketMaybeWithData>) => {
data.data.bestBidPrice, return data?.data?.markPrice === undefined
data.decimalPlaces ? undefined
), : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
}, },
{ valueFormatter: ({
headerName: t('Best offer'), data,
field: 'data.bestOfferPrice', }: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
type: 'rightAligned', data?.data?.bestOfferPrice === undefined
cellRenderer: 'PriceFlashCell', ? '-'
filter: 'agNumberColumnFilter', : addDecimalsFormatNumber(
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => { data.data.markPrice,
return data?.data?.bestOfferPrice === undefined data.decimalPlaces
? undefined ),
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
}, },
valueFormatter: ({ {
data, headerName: t('Settlement asset'),
}: VegaValueFormatterParams< field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
MarketMaybeWithData, cellRenderer: ({
'data.bestOfferPrice' data,
>) => }: VegaICellRendererParams<
data?.data?.bestOfferPrice === undefined MarketMaybeWithData,
? '-' 'tradableInstrument.instrument.product.settlementAsset.symbol'
: addDecimalsFormatNumber( >) => {
data.data.bestOfferPrice, const value =
data.decimalPlaces data?.tradableInstrument.instrument.product.settlementAsset;
), return value ? (
}, <ButtonLink
{ onClick={(e) => {
headerName: t('Mark price'), openAssetDetailsDialog(value.id, e.target as HTMLElement);
field: 'data.markPrice', }}
type: 'rightAligned', >
cellRenderer: 'PriceFlashCell', {value.symbol}
filter: 'agNumberColumnFilter', </ButtonLink>
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => { ) : (
return data?.data?.markPrice === undefined ''
? undefined );
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); },
}, },
valueFormatter: ({ {
data, colId: 'market-actions',
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) => field: 'id',
data?.data?.bestOfferPrice === undefined ...COL_DEFS.actions,
? '-' cellRenderer: ({
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces), data,
}, }: VegaICellRendererParams<MarketMaybeWithData>) => {
{ if (!data) return null;
headerName: t('Settlement asset'), return (
field: 'tradableInstrument.instrument.product.settlementAsset.symbol', <MarketActionsDropdown
cellRenderer: ({ marketId={data.id}
data, assetId={
}: VegaICellRendererParams< data.tradableInstrument.instrument.product.settlementAsset.id
MarketMaybeWithData, }
'tradableInstrument.instrument.product.settlementAsset.symbol' />
>) => { );
const value = },
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
}, },
}, ]),
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketActionsDropdown
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
],
[onMarketClick, openAssetDetailsDialog] [onMarketClick, openAssetDetailsDialog]
); );
}; };

View File

@ -27,8 +27,9 @@ import {
filterAndSortMarkets, filterAndSortMarkets,
} from './market-utils'; } from './market-utils';
import { MarketsDocument } from './__generated__/markets'; import { MarketsDocument } from './__generated__/markets';
import type { Candle } from './market-candles-provider'; import type { Candle } from './market-candles-provider';
import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket';
import { SuccessorMarketIdsDocument } from './__generated__';
export type Market = MarketFieldsFragment; export type Market = MarketFieldsFragment;
@ -240,3 +241,34 @@ export const useMarketList = () => {
reload, 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;
};

View File

@ -14,6 +14,23 @@ import { createProposalListFieldsFragment } from '../../lib/proposals-data-provi
import type { ProposalsListQuery } from '../../lib'; import type { ProposalsListQuery } from '../../lib';
import { ProposalsListDocument } from '../../lib'; import { ProposalsListDocument } from '../../lib';
import type { PartialDeep } from 'type-fest'; 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', () => { describe('ProposalsList', () => {
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => { const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
@ -64,13 +81,15 @@ describe('ProposalsList', () => {
return mock; return mock;
}; };
beforeEach(() => {
jest.clearAllMocks();
});
it('should be properly rendered', async () => { it('should be properly rendered', async () => {
const mock = createProposalsMock(); const mock = createProposalsMock();
await act(() => { await act(() => {
render( render(
<MockedProvider mocks={[mock]}> <MockedProvider mocks={[mock]}>
<ProposalsList /> <ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider> </MockedProvider>
); );
}); });
@ -82,31 +101,55 @@ describe('ProposalsList', () => {
}); });
it('some of states should be filtered out', async () => { it('some of states should be filtered out', async () => {
const proposalNode = createProposalListFieldsFragment({
id: 'id-1',
state: Types.ProposalState.STATE_ENACTED,
});
const mock = createProposalsMock({ const mock = createProposalsMock({
proposalsConnection: { proposalsConnection: {
edges: [ edges: [
{ {
__typename: 'ProposalEdge', __typename: 'ProposalEdge',
node: createProposalListFieldsFragment({ node: {
id: 'id-1', ...proposalNode,
state: Types.ProposalState.STATE_ENACTED, terms: {
}), ...proposalNode.terms,
change: {
...proposalNode.terms.change,
},
},
},
}, },
], ],
}, },
}); } as PartialDeep<ProposalsListQuery>);
await act(() => { await act(() => {
render( render(
<MockedProvider mocks={[mock]}> <MockedProvider mocks={[mock]}>
<ProposalsList /> <ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider> </MockedProvider>
); );
}); });
const container = document.querySelector('.ag-center-cols-container'); const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => { await waitFor(() => {
expect(container).toBeInTheDocument(); 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 () => { it('empty response should causes no data message display', async () => {
@ -129,10 +172,55 @@ describe('ProposalsList', () => {
await act(() => { await act(() => {
render( render(
<MockedProvider mocks={[mock]}> <MockedProvider mocks={[mock]}>
<ProposalsList /> <ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider> </MockedProvider>
); );
}); });
expect(await screen.findByText('No markets')).toBeInTheDocument(); 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<ProposalsListQuery> = {
request: {
query: ProposalsListDocument,
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
},
result: {
data: {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: [],
},
},
},
};
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
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');
});
}); });
}); });

View File

@ -1,3 +1,4 @@
import React from 'react';
import { useRef } from 'react'; import { useRef } from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@ -17,7 +18,13 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
].includes(proposal.state) ].includes(proposal.state)
); );
export const ProposalsList = () => { interface ProposalListProps {
SuccessorMarketRenderer: React.FC<{ value: string }>;
}
export const ProposalsList = ({
SuccessorMarketRenderer,
}: ProposalListProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const { data } = useProposalsListQuery({ const { data } = useProposalsListQuery({
variables: { variables: {
@ -41,6 +48,7 @@ export const ProposalsList = () => {
getRowId={({ data }) => data.id} getRowId={({ data }) => data.id}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')} overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer }}
/> />
</div> </div>
); );

View File

@ -2,7 +2,8 @@ import { useMemo } from 'react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import { COL_DEFS, DateRangeFilter, SetFilter } from '@vegaprotocol/datagrid'; 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 { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { import {
@ -32,7 +33,7 @@ export const useColumnDefs = () => {
const cellCss = 'grid h-full items-center'; const cellCss = 'grid h-full items-center';
const columnDefs: ColDef[] = useMemo(() => { const columnDefs: ColDef[] = useMemo(() => {
return [ return compact([
{ {
colId: 'market', colId: 'market',
headerName: t('Market'), headerName: t('Market'),
@ -83,6 +84,13 @@ export const useColumnDefs = () => {
set: ProposalStateMapping, set: ProposalStateMapping,
}, },
}, },
FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Parent market'),
field: 'id',
colId: 'parentMarket',
cellRenderer: 'SuccessorMarketRenderer',
cellRendererParams: { parent: true },
},
{ {
colId: 'voting', colId: 'voting',
headerName: t('Voting'), headerName: t('Voting'),
@ -146,7 +154,7 @@ export const useColumnDefs = () => {
}, },
flex: 1, flex: 1,
}, },
]; ]);
}, [VEGA_TOKEN_URL, requiredMajorityPercentage]); }, [VEGA_TOKEN_URL, requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => { const defaultColDef: ColDef = useMemo(() => {