diff --git a/apps/trading/client-pages/market/trade-grid.tsx b/apps/trading/client-pages/market/trade-grid.tsx index 37ee75754..2fe417333 100644 --- a/apps/trading/client-pages/market/trade-grid.tsx +++ b/apps/trading/client-pages/market/trade-grid.tsx @@ -17,7 +17,10 @@ import { usePaneLayout, } from '../../components/resizable-grid'; import { TradingViews } from './trade-views'; -import { MarketSuccessorBanner } from '../../components/market-banner'; +import { + MarketSuccessorBanner, + MarketSuccessorProposalBanner, +} from '../../components/market-banner'; import { FLAGS } from '@vegaprotocol/environment'; interface TradeGridProps { @@ -162,7 +165,12 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => { return (
- {FLAGS.SUCCESSOR_MARKETS && } + {FLAGS.SUCCESSOR_MARKETS && ( + <> + + + + )}
diff --git a/apps/trading/client-pages/market/trade-panels.tsx b/apps/trading/client-pages/market/trade-panels.tsx index 703b6a758..037618fb1 100644 --- a/apps/trading/client-pages/market/trade-panels.tsx +++ b/apps/trading/client-pages/market/trade-panels.tsx @@ -12,7 +12,10 @@ import { Splash } from '@vegaprotocol/ui-toolkit'; import { NO_MARKET } from './constants'; import AutoSizer from 'react-virtualized-auto-sizer'; import classNames from 'classnames'; -import { MarketSuccessorBanner } from '../../components/market-banner'; +import { + MarketSuccessorBanner, + MarketSuccessorProposalBanner, +} from '../../components/market-banner'; import { FLAGS } from '@vegaprotocol/environment'; interface TradePanelsProps { @@ -65,7 +68,12 @@ export const TradePanels = ({ return (
- {FLAGS.SUCCESSOR_MARKETS && } + {FLAGS.SUCCESSOR_MARKETS && ( + <> + + + + )}
diff --git a/apps/trading/components/market-banner/index.ts b/apps/trading/components/market-banner/index.ts index 52d75d62a..ed705d434 100644 --- a/apps/trading/components/market-banner/index.ts +++ b/apps/trading/components/market-banner/index.ts @@ -1 +1,2 @@ export * from './market-successor-banner'; +export * from './market-successor-proposal-banner'; diff --git a/apps/trading/components/market-banner/market-successor-proposal-banner.spec.tsx b/apps/trading/components/market-banner/market-successor-proposal-banner.spec.tsx new file mode 100644 index 000000000..4f7138694 --- /dev/null +++ b/apps/trading/components/market-banner/market-successor-proposal-banner.spec.tsx @@ -0,0 +1,155 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import type { SingleExecutionResult } from '@apollo/client'; +import type { MockedResponse } from '@apollo/react-testing'; +import { MockedProvider } from '@apollo/react-testing'; +import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner'; +import type { SuccessorProposalsListQuery } from '@vegaprotocol/proposals'; +import { SuccessorProposalsListDocument } from '@vegaprotocol/proposals'; + +const marketProposalMock: MockedResponse = { + request: { + query: SuccessorProposalsListDocument, + }, + result: { + data: { + proposalsConnection: { + edges: [ + { + node: { + __typename: 'Proposal', + id: 'proposal-1', + terms: { + __typename: 'ProposalTerms', + change: { + __typename: 'NewMarket', + instrument: { + name: 'New proposal of the market successor', + }, + successorConfiguration: { + parentMarketId: 'marketId', + }, + }, + }, + }, + }, + ], + }, + }, + }, +}; + +describe('MarketSuccessorProposalBanner', () => { + it('should display single proposal', async () => { + render( + + + + ); + await waitFor(() => { + expect( + screen.getByText('A successors to this market has been proposed') + ).toBeInTheDocument(); + }); + expect( + screen + .getByRole('link') + .getAttribute('href') + ?.endsWith('/proposals/proposal-1') ?? false + ).toBe(true); + }); + it('should display plural proposals', async () => { + const dualProposalMock = { + ...marketProposalMock, + result: { + ...marketProposalMock.result, + data: { + proposalsConnection: { + edges: [ + ...(( + marketProposalMock?.result as SingleExecutionResult + )?.data?.proposalsConnection?.edges ?? []), + { + node: { + __typename: 'Proposal', + id: 'proposal-2', + terms: { + __typename: 'ProposalTerms', + change: { + __typename: 'NewMarket', + instrument: { + name: 'New second proposal of the market successor', + }, + successorConfiguration: { + parentMarketId: 'marketId', + }, + }, + }, + }, + }, + ], + }, + }, + }, + }; + + render( + + + + ); + await waitFor(() => { + expect( + screen.getByText('Successors to this market have been proposed') + ).toBeInTheDocument(); + }); + expect( + screen + .getAllByRole('link')[0] + .getAttribute('href') + ?.endsWith('/proposals/proposal-1') ?? false + ).toBe(true); + expect( + screen + .getAllByRole('link')[1] + .getAttribute('href') + ?.endsWith('/proposals/proposal-2') ?? false + ).toBe(true); + }); + + it('banner should be hidden because no proposals', () => { + const { container } = render( + + + + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('banner should be hidden because no proposals for the market', () => { + const { container } = render( + + + + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('banner should be hidden after user close click', async () => { + const { container } = render( + + + + ); + await waitFor(() => { + expect( + screen.getByText('A successors to this market has been proposed') + ).toBeInTheDocument(); + }); + await act(() => { + screen.getByTestId('notification-banner-close').click(); + }); + await waitFor(() => { + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/apps/trading/components/market-banner/market-successor-proposal-banner.tsx b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx new file mode 100644 index 000000000..68104fe98 --- /dev/null +++ b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import type { + SuccessorProposalListFieldsFragment, + NewMarketSuccessorFieldsFragment, +} from '@vegaprotocol/proposals'; +import { useSuccessorProposalsListQuery } from '@vegaprotocol/proposals'; +import { + ExternalLink, + Intent, + NotificationBanner, +} from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/i18n'; +import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; + +export const MarketSuccessorProposalBanner = ({ + marketId, +}: { + marketId?: string; +}) => { + const { data: proposals } = useSuccessorProposalsListQuery({ + skip: !marketId, + }); + const successors = + proposals?.proposalsConnection?.edges + ?.map((item) => item?.node as SuccessorProposalListFieldsFragment) + .filter( + (item: SuccessorProposalListFieldsFragment) => + (item.terms?.change as NewMarketSuccessorFieldsFragment) + ?.successorConfiguration?.parentMarketId === marketId + ) ?? []; + const [visible, setVisible] = useState(true); + const tokenLink = useLinks(DApp.Token); + if (visible && successors.length) { + return ( + { + setVisible(false); + }} + > +
+ {successors.length === 1 + ? t('A successors to this market has been proposed') + : t('Successors to this market have been proposed')} +
+
+ {successors.length === 1 + ? t('Check out the terms of the proposal and vote:') + : t('Check out the terms of the proposals and vote:')}{' '} + {successors.map((item, i) => { + const externalLink = tokenLink( + TOKEN_PROPOSAL.replace(':id', item.id || '') + ); + return ( + <> + + { + (item.terms?.change as NewMarketSuccessorFieldsFragment) + ?.instrument.name + } + + {i < successors.length - 1 && ', '} + + ); + })} +
+
+ ); + } + return null; +}; diff --git a/libs/datagrid/src/lib/cells/centered-grid-cell.tsx b/libs/datagrid/src/lib/cells/centered-grid-cell.tsx index bcd0afbc4..52aa2a126 100644 --- a/libs/datagrid/src/lib/cells/centered-grid-cell.tsx +++ b/libs/datagrid/src/lib/cells/centered-grid-cell.tsx @@ -11,6 +11,6 @@ export const CenteredGridCellWrapper = ({
-
{children}
+
{children}
); diff --git a/libs/markets/src/lib/__generated__/SuccessorMarket.ts b/libs/markets/src/lib/__generated__/SuccessorMarket.ts index d41c58fe1..82bb761c3 100644 --- a/libs/markets/src/lib/__generated__/SuccessorMarket.ts +++ b/libs/markets/src/lib/__generated__/SuccessorMarket.ts @@ -183,4 +183,4 @@ export function useSuccessorMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOp } export type SuccessorMarketQueryHookResult = ReturnType; export type SuccessorMarketLazyQueryHookResult = ReturnType; -export type SuccessorMarketQueryResult = Apollo.QueryResult; +export type SuccessorMarketQueryResult = Apollo.QueryResult; \ No newline at end of file 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 33e08b2f3..c04eb5e50 100644 --- a/libs/proposals/src/components/proposals-list/use-column-defs.tsx +++ b/libs/proposals/src/components/proposals-list/use-column-defs.tsx @@ -1,7 +1,12 @@ 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 { + CenteredGridCellWrapper, + COL_DEFS, + DateRangeFilter, + SetFilter, +} from '@vegaprotocol/datagrid'; import compact from 'lodash/compact'; import { useEnvironment, FLAGS } from '@vegaprotocol/environment'; import { getDateTimeFormat } from '@vegaprotocol/utils'; @@ -31,7 +36,6 @@ export const useColumnDefs = () => { return new BigNumber(requiredMajority).times(100); }, [params?.governance_proposal_market_requiredMajority]); - const cellCss = 'grid h-full items-center'; const columnDefs: ColDef[] = useMemo(() => { return compact([ { @@ -94,7 +98,6 @@ export const useColumnDefs = () => { { colId: 'voting', headerName: t('Voting'), - cellClass: 'flex justify-between leading-tight font-mono', cellRenderer: ({ data, }: VegaICellRendererParams) => { @@ -106,12 +109,12 @@ export const useColumnDefs = () => { ? new BigNumber(0) : yesTokens.multipliedBy(100).dividedBy(totalTokensVoted); return ( -
+ -
+ ); } return '-'; @@ -160,7 +163,6 @@ export const useColumnDefs = () => { const defaultColDef: ColDef = useMemo(() => { return { sortable: true, - cellClass: cellCss, resizable: true, filter: true, filterParams: { buttons: ['reset'] }, diff --git a/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql b/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql index 774fa73e1..dde320b6c 100644 --- a/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql +++ b/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql @@ -327,3 +327,33 @@ query ProposalsList($proposalType: ProposalType, $inState: ProposalState) { } } } + +fragment NewMarketSuccessorFields on NewMarket { + instrument { + name + } + successorConfiguration { + parentMarketId + } +} + +fragment SuccessorProposalListFields on Proposal { + id + terms { + change { + ... on NewMarket { + ...NewMarketSuccessorFields + } + } + } +} + +query SuccessorProposalsList { + proposalsConnection(proposalType: TYPE_NEW_MARKET, inState: STATE_OPEN) { + edges { + node { + ...SuccessorProposalListFields + } + } + } +} diff --git a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts index 3a84e24f2..8f4c71956 100644 --- a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts +++ b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts @@ -23,6 +23,15 @@ export type ProposalsListQueryVariables = Types.Exact<{ export type ProposalsListQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, requiredMajority: string, requiredParticipation: string, requiredLpMajority?: string | null, requiredLpParticipation?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', decimalPlaces: number, metadata?: Array | null, lpPriceRange: string, instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | null }, riskParameters: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } } } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset', assetId: string, quantum: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } } } } | null> | null } | null }; +export type NewMarketSuccessorFieldsFragment = { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null }; + +export type SuccessorProposalListFieldsFragment = { __typename?: 'Proposal', id?: string | null, terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } }; + +export type SuccessorProposalsListQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type SuccessorProposalsListQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } } } | null> | null } | null }; + export const NewMarketFieldsFragmentDoc = gql` fragment NewMarketFields on NewMarket { instrument { @@ -335,6 +344,28 @@ ${UpdateMarketFieldsFragmentDoc} ${NewAssetFieldsFragmentDoc} ${UpdateAssetFieldsFragmentDoc} ${UpdateNetworkParameterFielsFragmentDoc}`; +export const NewMarketSuccessorFieldsFragmentDoc = gql` + fragment NewMarketSuccessorFields on NewMarket { + instrument { + name + } + successorConfiguration { + parentMarketId + } +} + `; +export const SuccessorProposalListFieldsFragmentDoc = gql` + fragment SuccessorProposalListFields on Proposal { + id + terms { + change { + ... on NewMarket { + ...NewMarketSuccessorFields + } + } + } +} + ${NewMarketSuccessorFieldsFragmentDoc}`; export const ProposalsListDocument = gql` query ProposalsList($proposalType: ProposalType, $inState: ProposalState) { proposalsConnection(proposalType: $proposalType, inState: $inState) { @@ -374,4 +405,42 @@ export function useProposalsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti } export type ProposalsListQueryHookResult = ReturnType; export type ProposalsListLazyQueryHookResult = ReturnType; -export type ProposalsListQueryResult = Apollo.QueryResult; \ No newline at end of file +export type ProposalsListQueryResult = Apollo.QueryResult; +export const SuccessorProposalsListDocument = gql` + query SuccessorProposalsList { + proposalsConnection(proposalType: TYPE_NEW_MARKET, inState: STATE_OPEN) { + edges { + node { + ...SuccessorProposalListFields + } + } + } +} + ${SuccessorProposalListFieldsFragmentDoc}`; + +/** + * __useSuccessorProposalsListQuery__ + * + * To run a query within a React component, call `useSuccessorProposalsListQuery` and pass it any options that fit your needs. + * When your component renders, `useSuccessorProposalsListQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useSuccessorProposalsListQuery({ + * variables: { + * }, + * }); + */ +export function useSuccessorProposalsListQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(SuccessorProposalsListDocument, options); + } +export function useSuccessorProposalsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(SuccessorProposalsListDocument, options); + } +export type SuccessorProposalsListQueryHookResult = ReturnType; +export type SuccessorProposalsListLazyQueryHookResult = ReturnType; +export type SuccessorProposalsListQueryResult = Apollo.QueryResult; \ No newline at end of file