feat(trading): add market successor proposal banner (#4401)
This commit is contained in:
parent
736b262947
commit
54d6aaf56f
@ -17,7 +17,10 @@ import {
|
|||||||
usePaneLayout,
|
usePaneLayout,
|
||||||
} from '../../components/resizable-grid';
|
} from '../../components/resizable-grid';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TradeGridProps {
|
interface TradeGridProps {
|
||||||
@ -162,7 +165,12 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<div>
|
<div>
|
||||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
{FLAGS.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 p-0.5">
|
<div className="min-h-0 p-0.5">
|
||||||
|
@ -12,7 +12,10 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { NO_MARKET } from './constants';
|
import { NO_MARKET } from './constants';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TradePanelsProps {
|
interface TradePanelsProps {
|
||||||
@ -65,7 +68,12 @@ export const TradePanels = ({
|
|||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||||
<div>
|
<div>
|
||||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
{FLAGS.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './market-successor-banner';
|
export * from './market-successor-banner';
|
||||||
|
export * from './market-successor-proposal-banner';
|
||||||
|
@ -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<SuccessorProposalsListQuery> = {
|
||||||
|
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(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
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<SuccessorProposalsListQuery>
|
||||||
|
)?.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(
|
||||||
|
<MockedProvider mocks={[dualProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<MockedProvider>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden because no proposals for the market', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden after user close click', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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 (
|
||||||
|
<NotificationBanner
|
||||||
|
intent={Intent.Primary}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="uppercase mb-1">
|
||||||
|
{successors.length === 1
|
||||||
|
? t('A successors to this market has been proposed')
|
||||||
|
: t('Successors to this market have been proposed')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{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 (
|
||||||
|
<>
|
||||||
|
<ExternalLink href={externalLink} key={i}>
|
||||||
|
{
|
||||||
|
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||||
|
?.instrument.name
|
||||||
|
}
|
||||||
|
</ExternalLink>
|
||||||
|
{i < successors.length - 1 && ', '}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NotificationBanner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
@ -11,6 +11,6 @@ export const CenteredGridCellWrapper = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
|
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
|
||||||
>
|
>
|
||||||
<div className="self-center">{children}</div>
|
<div className="w-full self-center">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -183,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>;
|
@ -1,7 +1,12 @@
|
|||||||
import { useMemo } from 'react';
|
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 {
|
||||||
|
CenteredGridCellWrapper,
|
||||||
|
COL_DEFS,
|
||||||
|
DateRangeFilter,
|
||||||
|
SetFilter,
|
||||||
|
} from '@vegaprotocol/datagrid';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
|
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
@ -31,7 +36,6 @@ export const useColumnDefs = () => {
|
|||||||
return new BigNumber(requiredMajority).times(100);
|
return new BigNumber(requiredMajority).times(100);
|
||||||
}, [params?.governance_proposal_market_requiredMajority]);
|
}, [params?.governance_proposal_market_requiredMajority]);
|
||||||
|
|
||||||
const cellCss = 'grid h-full items-center';
|
|
||||||
const columnDefs: ColDef[] = useMemo(() => {
|
const columnDefs: ColDef[] = useMemo(() => {
|
||||||
return compact([
|
return compact([
|
||||||
{
|
{
|
||||||
@ -94,7 +98,6 @@ export const useColumnDefs = () => {
|
|||||||
{
|
{
|
||||||
colId: 'voting',
|
colId: 'voting',
|
||||||
headerName: t('Voting'),
|
headerName: t('Voting'),
|
||||||
cellClass: 'flex justify-between leading-tight font-mono',
|
|
||||||
cellRenderer: ({
|
cellRenderer: ({
|
||||||
data,
|
data,
|
||||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||||
@ -106,12 +109,12 @@ export const useColumnDefs = () => {
|
|||||||
? new BigNumber(0)
|
? new BigNumber(0)
|
||||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||||
return (
|
return (
|
||||||
<div className="uppercase flex h-full items-center justify-center">
|
<CenteredGridCellWrapper>
|
||||||
<VoteProgress
|
<VoteProgress
|
||||||
threshold={requiredMajorityPercentage}
|
threshold={requiredMajorityPercentage}
|
||||||
progress={yesPercentage}
|
progress={yesPercentage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CenteredGridCellWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return '-';
|
return '-';
|
||||||
@ -160,7 +163,6 @@ export const useColumnDefs = () => {
|
|||||||
const defaultColDef: ColDef = useMemo(() => {
|
const defaultColDef: ColDef = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
sortable: true,
|
sortable: true,
|
||||||
cellClass: cellCss,
|
|
||||||
resizable: true,
|
resizable: true,
|
||||||
filter: true,
|
filter: true,
|
||||||
filterParams: { buttons: ['reset'] },
|
filterParams: { buttons: ['reset'] },
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user