diff --git a/apps/explorer/src/app/routes/layout.tsx b/apps/explorer/src/app/routes/layout.tsx index f79c4dde1..7d310c356 100644 --- a/apps/explorer/src/app/routes/layout.tsx +++ b/apps/explorer/src/app/routes/layout.tsx @@ -133,7 +133,7 @@ export const ErrorBoundary = () => { ); }; -const GHOST = ( +export const GHOST = ( { + const errorTitle = '451 Unavailable'; + const errorMessage = + 'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.'; + + return ( +
+ +
+
+
{GHOST}
+

+ {errorTitle} +

+
+
+ {errorMessage} +
+
+
+ ); +}; + +const GHOST = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default RestrictedPage; diff --git a/apps/explorer/src/app/routes/route-names.tsx b/apps/explorer/src/app/routes/route-names.tsx index 8ee07a085..7e2f2175c 100644 --- a/apps/explorer/src/app/routes/route-names.tsx +++ b/apps/explorer/src/app/routes/route-names.tsx @@ -1,5 +1,6 @@ export const Routes = { HOME: '/', + RESTRICTED: '/restricted', TX: 'txs', BLOCKS: 'blocks', PARTIES: 'parties', diff --git a/apps/explorer/src/app/routes/router-config.tsx b/apps/explorer/src/app/routes/router-config.tsx index 6e1726779..3e79aa295 100644 --- a/apps/explorer/src/app/routes/router-config.tsx +++ b/apps/explorer/src/app/routes/router-config.tsx @@ -30,6 +30,7 @@ import { remove0x } from '@vegaprotocol/utils'; import { PartyAccountsByAsset } from './parties/id/accounts'; import { Disclaimer } from './pages/disclaimer'; import { FLAGS } from '@vegaprotocol/environment'; +import RestrictedPage from './restricted'; export type Navigable = { path: string; @@ -356,6 +357,14 @@ export const routerConfig: Route[] = [ ...validators, ], }, + { + path: Routes.RESTRICTED, + element: , + handle: { + name: t('Restricted'), + text: t('Restricted'), + }, + }, ]; export const router = createBrowserRouter(routerConfig); diff --git a/apps/governance/src/app.tsx b/apps/governance/src/app.tsx index 0bc19395e..39f77bd65 100644 --- a/apps/governance/src/app.tsx +++ b/apps/governance/src/app.tsx @@ -106,6 +106,7 @@ const Web3Container = ({ useEthWithdrawApprovalsManager(); return null; }; + const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [ store.connectors, store.initialize, @@ -239,6 +240,9 @@ const AppContainer = () => { store.setDialogOpen, ]); + // Hacky skip all the loading & web3 init for geo restricted users + const isRestricted = document?.location?.pathname?.includes('/restricted'); + useEffect(() => { if (ENV.dsn && telemetryOn === 'true') { Sentry.init({ @@ -304,6 +308,14 @@ const AppContainer = () => { } }, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]); + if (isRestricted) { + return ( + + + + ); + } + return ( diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index 66cf0a013..34374ad90 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -9,6 +9,7 @@ "pageTitleRedemptionTranche": "Redeem from Tranche", "pageTitleTranches": "Vesting tranches", "pageTitle404": "Page not found", + "pageTitle451": "451 unavailable", "pageTitleNotPermitted": "Can not proceed!", "pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key", "pageTitleProposals": "Proposals", diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx index def3377e3..01652c52e 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx @@ -12,18 +12,19 @@ import { generateProposal, generateYesVotes, } from '../../test-helpers/generate-proposals'; -import { ProposalHeader } from './proposal-header'; +import { ProposalHeader, NewTransferSummary } from './proposal-header'; import { lastWeek, nextWeek, mockWalletContext, createUserVoteQueryMock, } from '../../test-helpers/mocks'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import type { MockedResponse } from '@apollo/client/testing'; import { FLAGS } from '@vegaprotocol/environment'; import { BrowserRouter } from 'react-router-dom'; import { VoteState } from '../vote-details/use-user-vote'; +import { useNewTransferProposalDetails } from '@vegaprotocol/proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { MockedResponse } from '@apollo/client/testing'; jest.mock('@vegaprotocol/proposals', () => ({ ...jest.requireActual('@vegaprotocol/proposals'), @@ -31,6 +32,7 @@ jest.mock('@vegaprotocol/proposals', () => ({ code: 'PARENT_CODE', parentMarketId: 'PARENT_ID', }), + useNewTransferProposalDetails: jest.fn(), })); const renderComponent = ( @@ -418,3 +420,78 @@ describe('Proposal header', () => { expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument(); }); }); + +jest.mock('@vegaprotocol/proposals'); + +describe('', () => { + it('renders null if no details are provided', () => { + (useNewTransferProposalDetails as jest.Mock).mockReturnValue(null); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('handles OneOffGovernanceTransfer', () => { + (useNewTransferProposalDetails as jest.Mock).mockReturnValue({ + kind: { __typename: 'OneOffGovernanceTransfer', deliverOn: null }, + source: '0x123', + sourceType: 'wallet', + destination: '0x456', + destinationType: 'contract', + }); + const { getByText } = render(); + const textMatch = (content: string) => content.includes('One off transfer'); + expect(getByText(textMatch)).toBeInTheDocument(); + }); + + it('handles RecurringGovernanceTransfer', () => { + (useNewTransferProposalDetails as jest.Mock).mockReturnValue({ + kind: { + __typename: 'RecurringGovernanceTransfer', + startEpoch: 1, + endEpoch: 5, + }, + source: '0x123', + sourceType: 'wallet', + destination: '0x456', + destinationType: 'contract', + }); + const { getByText } = render(); + const textMatch = (content: string) => + content.includes('Recurring transfer'); + expect(getByText(textMatch)).toBeInTheDocument(); + }); + + it('should fallback to translated sourceType when source is not set', () => { + (useNewTransferProposalDetails as jest.Mock).mockReturnValue({ + kind: { + __typename: 'RecurringGovernanceTransfer', + startEpoch: 1, + endEpoch: 5, + }, + source: undefined, + sourceType: 'ACCOUNT_TYPE_GENERAL', + destination: '0x456', + destinationType: 'ACCOUNT_TYPE_GENERAL', + }); + + render(); + expect(screen.getByText('General account')).toBeInTheDocument(); + }); + + it('should fallback to translated destinationType when destination is not set', () => { + (useNewTransferProposalDetails as jest.Mock).mockReturnValue({ + kind: { + __typename: 'RecurringGovernanceTransfer', + startEpoch: 1, + endEpoch: 5, + }, + source: '0x123', + sourceType: 'ACCOUNT_TYPE_GENERAL', + destination: undefined, + destinationType: 'ACCOUNT_TYPE_GLOBAL_INSURANCE', + }); + + render(); + expect(screen.getByText('Global insurance account')).toBeInTheDocument(); + }); +}); diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx index 1524edeac..836b62822 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx @@ -237,7 +237,11 @@ export const ProposalHeader = ({ ); }; -const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => { +export const SuccessorCode = ({ + proposalId, +}: { + proposalId?: string | null; +}) => { const { t } = useTranslation(); const successor = useSuccessorMarketProposalDetails(proposalId); @@ -254,7 +258,11 @@ const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => { ) : null; }; -const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => { +export const NewTransferSummary = ({ + proposalId, +}: { + proposalId?: string | null; +}) => { const { t } = useTranslation(); const details = useNewTransferProposalDetails(proposalId); @@ -265,14 +273,21 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => { {GovernanceTransferKindMapping[details.kind.__typename]}{' '} {t('transfer from')}{' '} - {truncateMiddle(details.source) || t(details.sourceType)} + {details.source + ? truncateMiddle(details.source) + : t(details.sourceType)} {' '} - {t('to')} {truncateMiddle(details.destination)} + {t('to')}{' '} + + {details.destination + ? truncateMiddle(details.destination) + : t(details.destinationType)} + ); }; -const CancelTransferSummary = ({ +export const CancelTransferSummary = ({ proposalId, }: { proposalId?: string | null; diff --git a/apps/governance/src/routes/restricted/index.tsx b/apps/governance/src/routes/restricted/index.tsx new file mode 100644 index 000000000..f26fea629 --- /dev/null +++ b/apps/governance/src/routes/restricted/index.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next'; +import { useDocumentTitle } from '../../hooks/use-document-title'; +import type { RouteChildProps } from '..'; +import { BackgroundVideo } from '@vegaprotocol/ui-toolkit'; +import classNames from 'classnames'; + +const Restricted = ({ name }: RouteChildProps) => { + useDocumentTitle(name); + const { t } = useTranslation(); + const errorMessage = + 'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.'; + + return ( +
+ +
+
+
{GHOST}
+

+ {t('pageTitle451')} +

+
+
+ {errorMessage} +
+
+
+ ); +}; + +const GHOST = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default Restricted; diff --git a/apps/governance/src/routes/router-config.tsx b/apps/governance/src/routes/router-config.tsx index 90de63f6b..50eb347f9 100644 --- a/apps/governance/src/routes/router-config.tsx +++ b/apps/governance/src/routes/router-config.tsx @@ -4,6 +4,7 @@ import Home from './token'; import NotFound from './not-found'; import NotPermitted from './not-permitted'; import Routes from './routes'; +import Restricted from './restricted'; const LazyTranches = React.lazy( () => @@ -361,8 +362,13 @@ const routerConfig = [ element: , }, { - path: '*', + path: Routes.RESTRICTED, // Not lazy as loaded when a user first hits the site + element: , + }, + { + path: '*', + // Also not lazy as loaded when a user first hits the site element: , }, ...redirects, diff --git a/apps/governance/src/routes/routes.ts b/apps/governance/src/routes/routes.ts index df73ecdb1..edc18dcb7 100644 --- a/apps/governance/src/routes/routes.ts +++ b/apps/governance/src/routes/routes.ts @@ -7,6 +7,7 @@ const Routes = { PROPOSALS_REJECTED: '/proposals/rejected', PROTOCOL_UPGRADES: '/protocol-upgrades', NOT_PERMITTED: '/not-permitted', + RESTRICTED: '/restricted', NOT_FOUND: '/not-found', CONTRACTS: '/contracts', TOKEN: '/token', @@ -33,6 +34,10 @@ export const TOP_LEVEL_ROUTES = [ name: 'Rewards', path: Routes.REWARDS, }, + { + name: 'Restricted', + path: Routes.RESTRICTED, + }, ]; export const TOKEN_DROPDOWN_ROUTES = [ diff --git a/apps/trading-e2e/src/integration/trading-orders.cy.ts b/apps/trading-e2e/src/integration/trading-orders.cy.ts deleted file mode 100644 index ae79c684c..000000000 --- a/apps/trading-e2e/src/integration/trading-orders.cy.ts +++ /dev/null @@ -1,529 +0,0 @@ -import * as Schema from '@vegaprotocol/types'; -import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet'; -import { - updateOrder, - getSubscriptionMocks, -} from '../support/order-update-subscription'; -import { - testOrderCancellation, - testOrderAmendment, -} from '../support/order-validation'; - -const orderSymbol = 'instrument-code'; -const orderSize = 'size'; -const orderType = 'type'; -const orderStatus = 'status'; -const orderRemaining = 'remaining'; -const orderPrice = 'price'; -const orderTimeInForce = 'timeInForce'; -const orderUpdatedAt = 'updatedAt'; -const cancelOrderBtn = 'cancel'; -const cancelAllOrdersBtn = 'cancelAll'; -const editOrderBtn = 'edit'; - -describe('orders list', { tags: '@smoke', testIsolation: true }, () => { - beforeEach(() => { - const subscriptionMocks = getSubscriptionMocks(); - cy.spy(subscriptionMocks, 'OrdersUpdate'); - cy.mockTradingPage(); - cy.mockSubscription(subscriptionMocks); - cy.setVegaWallet(); - cy.visit('/#/markets/market-0'); - cy.getByTestId('All').click(); - cy.wait('@Markets'); - }); - - it('renders orders', () => { - cy.getByTestId('tab-orders').should('be.visible'); - cy.getByTestId(cancelAllOrdersBtn).should('be.visible'); - cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1); - cy.getByTestId(editOrderBtn).should('have.length.at.least', 1); - cy.getByTestId('tab-orders').within(() => { - cy.get(`[role='rowgroup']`) - .first() - .within(() => { - cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => { - cy.wrap($symbol).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => { - cy.wrap($remaining).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderSize}']`).each(($size) => { - cy.wrap($size).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderType}']`).each(($type) => { - cy.wrap($type).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderStatus}']`).each(($status) => { - cy.wrap($status).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderPrice}']`).each(($price) => { - cy.wrap($price).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => { - cy.wrap($timeInForce).invoke('text').should('not.be.empty'); - }); - - cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => { - cy.wrap($dateTime).invoke('text').should('not.be.empty'); - }); - }); - }); - }); - - it('partially filled orders should not show close/edit buttons', () => { - const partiallyFilledId = - '94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78'; - - cy.getByTestId('tab-orders').should('be.visible'); - cy.get('.ag-header-container').within(() => { - cy.get('[col-id="status"]').realHover(); - cy.get('[col-id="status"] .ag-icon-menu').click(); - }); - cy.contains('Partially Filled').click(); - cy.getByTestId('All').click(); - - cy.get(`[row-id="${partiallyFilledId}"]`) - .eq(0) - .within(() => { - cy.get(`[col-id='${orderStatus}']`).should( - 'have.text', - 'Partially Filled' - ); - cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7'); - cy.get(`[col-id='${orderSize}']`).should('have.text', '-10'); - cy.getByTestId(cancelOrderBtn).should('not.exist'); - cy.getByTestId(editOrderBtn).should('not.exist'); - }); - }); - - it('orders are sorted by most recent order', () => { - // 7003-MORD-002 - const expectedOrderList = [ - 'BTCUSD.MF21', - 'SOLUSD', - 'AAPL.MF21', - 'BTCUSD.MF21', - 'BTCUSD.MF21', - ]; - cy.get('.ag-header-container').within(() => { - cy.get('[col-id="status"]').realHover(); - cy.get('[col-id="status"] .ag-icon-menu').click(); - }); - cy.contains('Reset').click(); - cy.getByTestId('All').click(); - - cy.getByTestId('tab-orders') - .get( - `.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]` - ) - .should('have.length.at.least', expectedOrderList.length) - .then(($symbols) => { - const symbolNames: string[] = []; - cy.wrap($symbols) - .each(($symbol) => { - cy.wrap($symbol) - .invoke('text') - .then((text) => { - symbolNames.push(text); - }); - }) - .then(() => { - expect(symbolNames).to.include.ordered.members(expectedOrderList); - }); - }); - }); -}); - -describe('subscribe orders', { tags: '@smoke' }, () => { - let orderId = '0'; - beforeEach(() => { - const subscriptionMocks = getSubscriptionMocks(); - cy.spy(subscriptionMocks, 'OrdersUpdate'); - cy.mockTradingPage(); - cy.mockSubscription(subscriptionMocks); - cy.setVegaWallet(); - cy.visit('/#/markets/market-0'); - cy.getByTestId('All').click(); - cy.getByTestId('tab-orders').within(() => { - cy.get('[col-id="status"][role="columnheader"]') - .focus() - .find('.ag-header-cell-menu-button') - .click(); - cy.get('.ag-filter-apply-panel-button').click(); - }); - orderId = (parseInt(orderId, 10) + 1).toString(); - }); - // 7002-SORD-053 - // 7002-SORD-040 - // 7003-MORD-001 - - it('must see an active order', () => { - // 7002-SORD-041 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - }); - cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active'); - }); - - it('must see an expired order', () => { - // 7002-SORD-042 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_EXPIRED, - }); - cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired'); - }); - - it('must see a cancelled order', () => { - // 7002-SORD-043 - // NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction. - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_CANCELLED, - }); - cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled'); - }); - - it('must see a stopped order', () => { - // 7002-SORD-044 - // NOT COVERED: see an explanation of why stopped - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_STOPPED, - }); - cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped'); - }); - - it('must see a partially filled order', () => { - // 7002-SORD-045 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED, - size: '5', - remaining: '1', - }); - cy.getByTestId(`order-status-${orderId}`).should( - 'have.text', - 'Partially Filled' - ); - cy.getByTestId(`order-status-${orderId}`) - .parentsUntil(`.ag-row`) - .siblings(`[col-id=${orderRemaining}]`) - .should('have.text', '4'); - }); - - it('must see a filled order', () => { - // 7002-SORD-046 - // 7003-MORD-020 - // NOT COVERED: Must be able to see/link to all trades that were created from this order - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_FILLED, - }); - cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled'); - cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr'); - }); - - it('must see a rejected order', () => { - // 7002-SORD-047 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_REJECTED, - rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR, - }); - cy.getByTestId(`order-status-${orderId}`).should( - 'have.text', - 'Rejected: Internal error' - ); - }); - - it('must see a parked order', () => { - // 7002-SORD-048 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_PARKED, - }); - cy.getByTestId(`order-status-${orderId}`).should( - 'have.text', - 'Parked: Internal error' - ); - }); - - it('must see the size of the order and direction/side -', () => { - // 7003-MORD-003 - // 7003-MORD-004 - updateOrder({ - id: orderId, - size: '15', - side: Schema.Side.SIDE_SELL, - status: Schema.OrderStatus.STATUS_ACTIVE, - }); - cy.get(`[row-id=${orderId}]`) - .find(`[col-id="${orderSize}"]`) - .should('have.text', '-15'); - }); - - it('must see the size of the order and direction/side +', () => { - // 7003-MORD-003 - // 7003-MORD-004 - updateOrder({ - id: orderId, - size: '5', - side: Schema.Side.SIDE_BUY, - status: Schema.OrderStatus.STATUS_ACTIVE, - }); - cy.get(`[row-id=${orderId}]`) - .find(`[col-id="${orderSize}"]`) - .should('have.text', '+5'); - }); - - it('for limit typy must see the Limit price that was set on the order', () => { - // 7003-MORD-005 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - }); - cy.get(`[row-id=${orderId}]`) - .find('[col-id="price"]') - .should('have.text', '200.00'); - }); - - it('must see a pegged order - ask', () => { - updateOrder({ - id: orderId, - side: Schema.Side.SIDE_BUY, - peggedOrder: { - __typename: 'PeggedOrder', - reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK, - offset: '250000', - }, - }); - cy.get(`[row-id=${orderId}]`) - .find('[col-id="type"]') - .should('have.text', 'Ask - 2.50 Peg limit'); - }); - - it('must see a pegged order - bid', () => { - updateOrder({ - id: orderId, - side: Schema.Side.SIDE_SELL, - peggedOrder: { - __typename: 'PeggedOrder', - reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID, - offset: '100', - }, - }); - cy.get(`[row-id=${orderId}]`) - .find('[col-id="type"]') - .should('have.text', 'Bid + 0.001 Peg limit'); - }); - - it('must see a pegged order - mid', () => { - updateOrder({ - id: orderId, - side: Schema.Side.SIDE_SELL, - peggedOrder: { - __typename: 'PeggedOrder', - reference: Schema.PeggedReference.PEGGED_REFERENCE_MID, - offset: '0.5', - }, - }); - cy.get(`[row-id=${orderId}]`) - .find('[col-id="type"]') - .should('have.text', 'Mid + 0.00001 Peg limit'); - }); - - it('for market typy must not see a price for active or parked orders', () => { - // 7003-MORD-005 - updateOrder({ - id: orderId, - type: Schema.OrderType.TYPE_MARKET, - status: Schema.OrderStatus.STATUS_PARKED, - peggedOrder: null, - }); - cy.get(`[row-id=${orderId}]`) - .find('[col-id="price"]') - .should('have.text', '-'); - }); - - it('must see the time in force applied to the order', () => { - // 7003-MORD-006 - updateOrder({ - id: orderId, - type: Schema.OrderType.TYPE_MARKET, - status: Schema.OrderStatus.STATUS_ACTIVE, - timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, - }); - cy.get(`[row-id=${orderId}]`) - .find(`[col-id='${orderTimeInForce}']`) - .should('have.text', 'GTC'); - }); - - it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => { - // 7003-MORD-008 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: {}, - liquidityProvisionId: '6536', - }); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="cancel"]`) - .should('not.exist'); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="edit"]`) - .should('not.exist'); - }); - - it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => { - // 7003-MORD-008 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: {}, - liquidityProvisionId: '6536', - }); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="cancel"]`) - .should('not.exist'); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="edit"]`) - .should('not.exist'); - }); - - it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => { - // 7003-MORD-008 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: {}, - }); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="cancel"]`) - .should('not.exist'); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="edit"]`) - .should('not.exist'); - }); -}); - -describe('amend and cancel order', { tags: '@smoke' }, () => { - beforeEach(() => { - const subscriptionMocks = getSubscriptionMocks(); - cy.spy(subscriptionMocks, 'OrdersUpdate'); - cy.mockTradingPage(); - cy.mockSubscription(subscriptionMocks); - cy.setVegaWallet(); - cy.visit('/#/markets/market-0'); - cy.getByTestId('All').click(); - cy.getByTestId('tab-orders').within(() => { - cy.get('[col-id="status"][role="columnheader"]') - .focus() - .find('.ag-header-cell-menu-button') - .click(); - cy.get('.ag-filter-apply-panel-button').click(); - }); - cy.mockVegaWalletTransaction(); - }); - - const orderId = '1234567890'; - - // this test is flakey - it('must be able to amend the price of an order', () => { - // 7003-MORD-007 - // 7003-MORD-012 - // 7003-MORD-014 - // 7003-MORD-015 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: null, - liquidityProvisionId: null, - }); - cy.get(`[row-id=${orderId}]`) - .find('[data-testid="icon-edit"]') - .then(($btn) => { - cy.wrap($btn).click(); - cy.getByTestId('dialog-title').should('have.text', 'Edit order'); - cy.get('#limitPrice').focus().clear().type('100'); - cy.getByTestId('edit-order').find('[type="submit"]').click(); - const order: OrderAmendment = { - orderId: orderId, - marketId: 'market-0', - price: '10000000', - timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, - sizeDelta: 0, - }; - testOrderAmendment(order); - }); - }); - it('must be able to cancel an individual order', () => { - // 7003-MORD-007 - // 7003-MORD-009 - // 7003-MORD-010 - // 7003-MORD-011 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: null, - liquidityProvisionId: null, - }); - cy.get(`[row-id=${orderId}]`) - .find(`[data-testid="icon-cross"]`) - .then(($btn) => { - cy.wrap($btn).click({ force: true }); - const order: OrderCancellation = { - orderId: orderId, - marketId: 'market-0', - }; - testOrderCancellation(order); - }); - }); - it('must be able to cancel all orders on all markets', () => { - // 7003-MORD-009 - // 7003-MORD-010 - // 7003-MORD-011 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: null, - liquidityProvisionId: null, - }); - cy.get(`[data-testid="cancelAll"]`) - .should('have.text', 'Cancel all') - .then(($btn) => { - cy.wrap($btn).click({ force: true }); - const order: OrderCancellation = {}; - testOrderCancellation(order); - }); - }); - it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => { - // 7003-MORD-013 - updateOrder({ - id: orderId, - status: Schema.OrderStatus.STATUS_ACTIVE, - peggedOrder: null, - liquidityProvisionId: null, - }); - cy.get(`[row-id=${orderId}]`) - .find('[data-testid="icon-edit"]') - .then(($btn) => { - cy.wrap($btn).click({ force: true }); - cy.getByTestId('dialog-title').should('have.text', 'Edit order'); - cy.get('#limitPrice').focus().clear().type('0.111111'); - cy.getByTestId('edit-order').find('[type="submit"]').click(); - cy.getByTestId('input-error-text').should( - 'have.text', - 'Price accepts up to 5 decimal places' - ); - }); - }); -}); diff --git a/apps/trading/client-pages/market/market-header-stats.tsx b/apps/trading/client-pages/market/market-header-stats.tsx index ed9c29ce1..7906f6801 100644 --- a/apps/trading/client-pages/market/market-header-stats.tsx +++ b/apps/trading/client-pages/market/market-header-stats.tsx @@ -86,6 +86,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => { {market.tradableInstrument.instrument.product.__typename === 'Future' && ( } + menu={} > @@ -136,7 +136,7 @@ const MainGrid = memo( } + menu={} > diff --git a/apps/trading/client-pages/market/trade-panels.tsx b/apps/trading/client-pages/market/trade-panels.tsx index 782e08cfa..76415f9ef 100644 --- a/apps/trading/client-pages/market/trade-panels.tsx +++ b/apps/trading/client-pages/market/trade-panels.tsx @@ -43,7 +43,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => { return (
- +
); } diff --git a/apps/trading/client-pages/portfolio/AccountHistory.graphql b/apps/trading/client-pages/portfolio/AccountHistory.graphql deleted file mode 100644 index 1af923e50..000000000 --- a/apps/trading/client-pages/portfolio/AccountHistory.graphql +++ /dev/null @@ -1,39 +0,0 @@ -query AccountHistory( - $partyId: ID! - $assetId: ID! - $accountTypes: [AccountType!] - $dateRange: DateRange - $marketIds: [ID!] -) { - balanceChanges( - filter: { - partyIds: [$partyId] - accountTypes: $accountTypes - assetId: $assetId - marketIds: $marketIds - } - dateRange: $dateRange - ) { - edges { - node { - timestamp - partyId - balance - marketId - assetId - accountType - } - } - } -} - -query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) { - balanceChanges(filter: { partyIds: [$partyId] }, dateRange: $dateRange) { - edges { - node { - assetId - accountType - } - } - } -} diff --git a/apps/trading/client-pages/portfolio/__generated__/AccountHistory.ts b/apps/trading/client-pages/portfolio/__generated__/AccountHistory.ts deleted file mode 100644 index 4c91baf59..000000000 --- a/apps/trading/client-pages/portfolio/__generated__/AccountHistory.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as Types from '@vegaprotocol/types'; - -import { gql } from '@apollo/client'; -import * as Apollo from '@apollo/client'; -const defaultOptions = {} as const; -export type AccountHistoryQueryVariables = Types.Exact<{ - partyId: Types.Scalars['ID']; - assetId: Types.Scalars['ID']; - accountTypes?: Types.InputMaybe | Types.AccountType>; - dateRange?: Types.InputMaybe; - marketIds?: Types.InputMaybe | Types.Scalars['ID']>; -}>; - - -export type AccountHistoryQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', timestamp: any, partyId?: string | null, balance: string, marketId?: string | null, assetId?: string | null, accountType?: Types.AccountType | null } } | null> } }; - -export type AccountsWithBalanceQueryVariables = Types.Exact<{ - partyId: Types.Scalars['ID']; - dateRange?: Types.InputMaybe; -}>; - - -export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', assetId?: string | null, accountType?: Types.AccountType | null } } | null> } }; - - -export const AccountHistoryDocument = gql` - query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) { - balanceChanges( - filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds} - dateRange: $dateRange - ) { - edges { - node { - timestamp - partyId - balance - marketId - assetId - accountType - } - } - } -} - `; - -/** - * __useAccountHistoryQuery__ - * - * To run a query within a React component, call `useAccountHistoryQuery` and pass it any options that fit your needs. - * When your component renders, `useAccountHistoryQuery` 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 } = useAccountHistoryQuery({ - * variables: { - * partyId: // value for 'partyId' - * assetId: // value for 'assetId' - * accountTypes: // value for 'accountTypes' - * dateRange: // value for 'dateRange' - * marketIds: // value for 'marketIds' - * }, - * }); - */ -export function useAccountHistoryQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(AccountHistoryDocument, options); - } -export function useAccountHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(AccountHistoryDocument, options); - } -export type AccountHistoryQueryHookResult = ReturnType; -export type AccountHistoryLazyQueryHookResult = ReturnType; -export type AccountHistoryQueryResult = Apollo.QueryResult; -export const AccountsWithBalanceDocument = gql` - query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) { - balanceChanges(filter: {partyIds: [$partyId]}, dateRange: $dateRange) { - edges { - node { - assetId - accountType - } - } - } -} - `; - -/** - * __useAccountsWithBalanceQuery__ - * - * To run a query within a React component, call `useAccountsWithBalanceQuery` and pass it any options that fit your needs. - * When your component renders, `useAccountsWithBalanceQuery` 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 } = useAccountsWithBalanceQuery({ - * variables: { - * partyId: // value for 'partyId' - * dateRange: // value for 'dateRange' - * }, - * }); - */ -export function useAccountsWithBalanceQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(AccountsWithBalanceDocument, options); - } -export function useAccountsWithBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(AccountsWithBalanceDocument, options); - } -export type AccountsWithBalanceQueryHookResult = ReturnType; -export type AccountsWithBalanceLazyQueryHookResult = ReturnType; -export type AccountsWithBalanceQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/trading/client-pages/portfolio/account-history-container.tsx b/apps/trading/client-pages/portfolio/account-history-container.tsx deleted file mode 100644 index a9df4b50a..000000000 --- a/apps/trading/client-pages/portfolio/account-history-container.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { addDecimal, fromNanoSeconds } from '@vegaprotocol/utils'; -import { t } from '@vegaprotocol/i18n'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import compact from 'lodash/compact'; -import uniqBy from 'lodash/uniqBy'; -import type { ChangeEvent } from 'react'; -import { useCallback, useMemo, useState } from 'react'; -import type { AccountHistoryQuery } from './__generated__/AccountHistory'; -import { useAccountHistoryQuery } from './__generated__/AccountHistory'; -import * as Schema from '@vegaprotocol/types'; -import type { AssetFieldsFragment } from '@vegaprotocol/assets'; -import { useAssetsDataProvider } from '@vegaprotocol/assets'; -import { - AsyncRenderer, - Splash, - Toggle, - TradingButton, - TradingDropdown, - TradingDropdownContent, - TradingDropdownItem, - TradingDropdownTrigger, -} from '@vegaprotocol/ui-toolkit'; -import { AccountTypeMapping } from '@vegaprotocol/types'; -import { PriceChart } from 'pennant'; -import 'pennant/dist/style.css'; -import type { Account } from '@vegaprotocol/accounts'; -import { accountsDataProvider } from '@vegaprotocol/accounts'; -import { - useLocalStorageSnapshot, - useThemeSwitcher, -} from '@vegaprotocol/react-helpers'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { getAsset, type Market } from '@vegaprotocol/markets'; - -export const DateRange = { - RANGE_1D: '1D', - RANGE_7D: '7D', - RANGE_1M: '1M', - RANGE_3M: '3M', - RANGE_1Y: '1Y', - RANGE_YTD: 'YTD', - RANGE_ALL: 'All', -}; - -const dateRangeToggleItems = Object.entries(DateRange).map(([_, value]) => ({ - label: t(value), - value: value, -})); - -export const calculateStartDate = (range: string): string | undefined => { - const now = new Date(); - switch (range) { - case DateRange.RANGE_1D: - return new Date(now.setDate(now.getDate() - 1)).toISOString(); - case DateRange.RANGE_7D: - return new Date(now.setDate(now.getDate() - 7)).toISOString(); - case DateRange.RANGE_1M: - return new Date(now.setMonth(now.getMonth() - 1)).toISOString(); - case DateRange.RANGE_3M: - return new Date(now.setMonth(now.getMonth() - 3)).toISOString(); - case DateRange.RANGE_1Y: - return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString(); - case DateRange.RANGE_YTD: - return new Date(now.setMonth(0)).toISOString(); - default: - return undefined; - } -}; - -export const AccountHistoryContainer = () => { - const { pubKey } = useVegaWallet(); - const { data: assets } = useAssetsDataProvider(); - - if (!pubKey) { - return {t('Please connect Vega wallet')}; - } - - return ( - - {assets && } - - ); -}; - -const AccountHistoryManager = ({ - pubKey, - assetData, -}: { - pubKey: string; - assetData: AssetFieldsFragment[]; -}) => { - const [accountType, setAccountType] = useState( - Schema.AccountType.ACCOUNT_TYPE_GENERAL - ); - - const variablesForOneTimeQuery = useMemo( - () => ({ - partyId: pubKey, - }), - [pubKey] - ); - - const { data: accounts } = useDataProvider({ - dataProvider: accountsDataProvider, - variables: variablesForOneTimeQuery, - skip: !pubKey, - }); - - const assetIds = useMemo( - () => accounts?.map((e) => e?.asset?.id) || [], - [accounts] - ); - - const assets = useMemo( - () => - assetData - .filter((a) => assetIds.includes(a.id)) - .sort((a, b) => a.name.localeCompare(b.name)), - [assetData, assetIds] - ); - const [assetId, setAssetId] = useLocalStorageSnapshot( - 'account-history-active-asset-id' - ); - - const asset = useMemo( - () => assets.find((a) => a.id === assetId) || assets[0], - [assetId, assets] - ); - - const [range, setRange] = useState( - DateRange.RANGE_1M - ); - const [market, setMarket] = useState(null); - - const marketFilterCb = useCallback( - (item: Market) => { - const itemAsset = getAsset(item); - return !asset?.id || itemAsset?.id === asset?.id; - }, - [asset?.id] - ); - const markets = useMemo(() => { - const arr = - accounts - ?.filter((item: Account) => Boolean(item && item.market)) - .map((item) => item.market as Market) ?? null; - return arr - ? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) => - a.tradableInstrument.instrument.code.localeCompare( - b.tradableInstrument.instrument.code - ) - ) - : null; - }, [accounts, marketFilterCb]); - - const resolveMarket = useCallback( - (m: Market) => { - setMarket(m); - const itemAsset = getAsset(m); - const newAssetId = itemAsset?.id; - const newAsset = assets.find((item) => item.id === newAssetId); - if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) { - setAssetId(newAsset.id); - } - }, - [asset, assets, setAssetId] - ); - - const variables = useMemo( - () => ({ - partyId: pubKey, - assetId: asset?.id || '', - accountTypes: accountType ? [accountType] : undefined, - dateRange: - range === 'All' ? undefined : { start: calculateStartDate(range) }, - marketIds: market?.id ? [market.id] : undefined, - }), - [pubKey, asset, accountType, range, market?.id] - ); - - const { data } = useAccountHistoryQuery({ - variables, - skip: !asset || !pubKey, - }); - - return ( -
-
-
- - - {accountType - ? `${ - AccountTypeMapping[ - accountType as keyof typeof Schema.AccountType - ] - } Account` - : t('Select account type')} - - - } - > - - {[ - Schema.AccountType.ACCOUNT_TYPE_GENERAL, - Schema.AccountType.ACCOUNT_TYPE_BOND, - Schema.AccountType.ACCOUNT_TYPE_MARGIN, - ].map((type) => ( - { - setAccountType(type as Schema.AccountType); - - // if not a margin account clear any market selection - if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) { - setMarket(null); - } - }} - > - {AccountTypeMapping[type as keyof typeof Schema.AccountType]} - - ))} - - - - - - {asset ? asset.symbol : t('Select asset')} - - - } - > - - {assets.map((a) => ( - { - setAssetId(a.id); - - // if the selected asset is different to the selected market clear the market - if (market && a.id !== getAsset(market).id) { - setMarket(null); - } - }} - > - {a.symbol} - - ))} - - - - - {market - ? market.tradableInstrument.instrument.code - : t('Select market')} - - - } - > - - {market && ( - setMarket(null)}> - {t('All markets')} - - )} - {markets?.map((m) => ( - resolveMarket(m)} - > - {m.tradableInstrument.instrument.code} - - ))} - - -
-
- ) => - setRange(e.target.value as keyof typeof DateRange) - } - size="sm" - /> -
-
-
- {asset && ( -
- -
- )} -
-
- ); -}; - -export const AccountHistoryChart = ({ - data, - accountType, - asset, -}: { - data: AccountHistoryQuery | undefined; - accountType: Schema.AccountType; - asset: AssetFieldsFragment; -}) => { - const { theme } = useThemeSwitcher(); - const values: { cols: [string, string]; rows: [Date, number][] } | null = - useMemo(() => { - if (!data?.balanceChanges.edges.length) { - return null; - } - - const valuesData = compact(data.balanceChanges.edges) - .reduce((acc, edge) => { - if (edge.node.accountType === accountType) { - acc?.push({ - datetime: fromNanoSeconds(edge.node.timestamp), - balance: Number(addDecimal(edge.node.balance, asset.decimals)), - }); - } - return acc; - }, [] as { datetime: Date; balance: number }[]) - .reverse(); - return { - cols: ['Date', `${asset.symbol} account balance`], - rows: compact(valuesData).map((d) => [d.datetime, d.balance]), - }; - }, [accountType, asset.decimals, asset.symbol, data?.balanceChanges.edges]); - - if (!data || !values?.rows.length) { - return {t('No account history data')}; - } - - return ; -}; diff --git a/apps/trading/client-pages/portfolio/portfolio.tsx b/apps/trading/client-pages/portfolio/portfolio.tsx index d7d9f883d..31bc80def 100644 --- a/apps/trading/client-pages/portfolio/portfolio.tsx +++ b/apps/trading/client-pages/portfolio/portfolio.tsx @@ -15,7 +15,6 @@ import { PositionsMenu } from '../../components/positions-menu'; import { WithdrawalsContainer } from '../../components/withdrawals-container'; import { OrdersContainer } from '../../components/orders-container'; import { LedgerContainer } from '../../components/ledger-container'; -import { AccountHistoryContainer } from './account-history-container'; import { ResizableGrid, ResizableGridPanel, @@ -66,10 +65,7 @@ export const Portfolio = () => { - - - - + { + const now = new Date(); + switch (range) { + case DateRange.RANGE_1D: + return new Date(now.setDate(now.getDate() - 1)).toISOString(); + case DateRange.RANGE_7D: + return new Date(now.setDate(now.getDate() - 7)).toISOString(); + case DateRange.RANGE_1M: + return new Date(now.setMonth(now.getMonth() - 1)).toISOString(); + case DateRange.RANGE_3M: + return new Date(now.setMonth(now.getMonth() - 3)).toISOString(); + case DateRange.RANGE_1Y: + return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString(); + case DateRange.RANGE_YTD: + return new Date(now.setMonth(0)).toISOString(); + default: + return undefined; + } +}; + +const DateRange = { + RANGE_1D: '1D', + RANGE_7D: '7D', + RANGE_1M: '1M', + RANGE_3M: '3M', + RANGE_1Y: '1Y', + RANGE_YTD: 'YTD', + RANGE_ALL: 'All', +}; export const FundingContainer = ({ marketId }: { marketId: string }) => { const { theme } = useThemeSwitcher(); diff --git a/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx b/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx index 3aedcd3ee..59f62c588 100644 --- a/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx +++ b/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx @@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js'; import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity'; import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types'; import { - addDecimalsFormatNumber, + addDecimalsFormatNumberQuantum, formatNumberPercentage, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; @@ -30,12 +30,14 @@ interface Props { marketId?: string; noUpdate?: boolean; assetDecimals: number; + quantum: string; } export const MarketLiquiditySupplied = ({ marketId, assetDecimals, noUpdate = false, + quantum, }: Props) => { const [market, setMarket] = useState(); const { params } = useNetworkParams([ @@ -79,11 +81,12 @@ export const MarketLiquiditySupplied = ({ }); const supplied = market?.suppliedStake - ? addDecimalsFormatNumber( + ? addDecimalsFormatNumberQuantum( new BigNumber(market?.suppliedStake) .multipliedBy(stakeToCcyVolume || 1) .toString(), - assetDecimals + assetDecimals, + quantum ) : '-'; @@ -108,9 +111,10 @@ export const MarketLiquiditySupplied = ({ {t('Supplied stake')} {market?.suppliedStake - ? addDecimalsFormatNumber( - new BigNumber(market?.suppliedStake).toString(), - assetDecimals + ? addDecimalsFormatNumberQuantum( + market?.suppliedStake, + assetDecimals, + quantum ) : '-'} @@ -119,9 +123,10 @@ export const MarketLiquiditySupplied = ({ {t('Target stake')} {market?.targetStake - ? addDecimalsFormatNumber( - new BigNumber(market?.targetStake).toString(), - assetDecimals + ? addDecimalsFormatNumberQuantum( + market?.targetStake, + assetDecimals, + quantum ) : '-'} diff --git a/apps/trading/components/market-banner/market-successor-proposal-banner.tsx b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx index 9cf78f540..f2d140770 100644 --- a/apps/trading/components/market-banner/market-successor-proposal-banner.tsx +++ b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx @@ -1,6 +1,9 @@ import { Fragment, useState } from 'react'; -import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals'; -import { useMarketViewProposals } from '@vegaprotocol/proposals'; +import { + marketViewProposalsDataProvider, + type NewMarketSuccessorFieldsFragment, +} from '@vegaprotocol/proposals'; + import { ExternalLink, Intent, @@ -9,17 +12,20 @@ import { import { t } from '@vegaprotocol/i18n'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import * as Types from '@vegaprotocol/types'; +import { useDataProvider } from '@vegaprotocol/data-provider'; export const MarketSuccessorProposalBanner = ({ marketId, }: { marketId?: string; }) => { - const proposals = useMarketViewProposals({ + const { data: proposals } = useDataProvider({ + dataProvider: marketViewProposalsDataProvider, skip: !marketId, - inState: Types.ProposalState.STATE_OPEN, - proposalType: Types.ProposalType.TYPE_NEW_MARKET, - typename: 'NewMarket', + variables: { + inState: Types.ProposalState.STATE_OPEN, + proposalType: Types.ProposalType.TYPE_NEW_MARKET, + }, }); const successors = diff --git a/apps/trading/components/market-banner/market-termination-banner.spec.tsx b/apps/trading/components/market-banner/market-termination-banner.spec.tsx index e7c806284..44bdb3966 100644 --- a/apps/trading/components/market-banner/market-termination-banner.spec.tsx +++ b/apps/trading/components/market-banner/market-termination-banner.spec.tsx @@ -20,10 +20,13 @@ const marketMock = { }, } as Market; -const proposalMock: MockedResponse = { +const passedProposalMock: MockedResponse = { request: { query: MarketViewProposalsDocument, - variables: { inState: Types.ProposalState.STATE_PASSED }, + variables: { + inState: Types.ProposalState.STATE_PASSED, + proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE, + }, }, result: { data: { @@ -31,7 +34,7 @@ const proposalMock: MockedResponse = { edges: [ { node: { - id: 'first-id', + id: '1', state: Types.ProposalState.STATE_PASSED, terms: { closingDatetime: '2023-09-27T11:48:18Z', @@ -56,7 +59,7 @@ const proposalMock: MockedResponse = { }, { node: { - id: 'second-id', + id: '2', state: Types.ProposalState.STATE_PASSED, terms: { closingDatetime: '2023-09-27T11:48:18Z', @@ -84,7 +87,99 @@ const proposalMock: MockedResponse = { }, }, }; -const mocks: MockedResponse[] = [proposalMock]; +const openProposalMock: MockedResponse = { + request: { + query: MarketViewProposalsDocument, + variables: { + inState: Types.ProposalState.STATE_OPEN, + proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE, + }, + }, + result: { + data: { + proposalsConnection: { + edges: [ + { + node: { + id: '3', + state: Types.ProposalState.STATE_OPEN, + terms: { + closingDatetime: '2023-09-27T11:48:18Z', + enactmentDatetime: '2023-10-01T11:48:18', + change: { + __typename: 'UpdateMarketState', + updateType: + Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE, + price: '', + market: { + id: 'market-3', + tradableInstrument: { + instrument: { + name: 'Market three name', + code: 'Market three', + }, + }, + }, + }, + }, + }, + }, + { + node: { + id: '4', + state: Types.ProposalState.STATE_OPEN, + terms: { + closingDatetime: '2023-09-27T11:48:18Z', + enactmentDatetime: '2023-10-11T11:48:18', + change: { + __typename: 'UpdateMarketState', + updateType: + Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE, + price: '', + market: { + id: 'market-3', + tradableInstrument: { + instrument: { + name: 'Market three name', + code: 'Market three', + }, + }, + }, + }, + }, + }, + }, + { + node: { + id: '5', + state: Types.ProposalState.STATE_OPEN, + terms: { + closingDatetime: '2023-09-27T11:48:18Z', + enactmentDatetime: '2023-10-01T11:48:18', + change: { + __typename: 'UpdateMarketState', + updateType: + Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE, + price: '', + market: { + id: 'market-4', + tradableInstrument: { + instrument: { + name: 'Market four name', + code: 'Market four', + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, +}; +const mocks: MockedResponse[] = [passedProposalMock, openProposalMock]; describe('MarketTerminationBanner', () => { beforeAll(() => { @@ -108,4 +203,28 @@ describe('MarketTerminationBanner', () => { screen.getByTestId('termination-warning-banner-market-1') ).toBeInTheDocument(); }); + + it('should render link to proposals', async () => { + const { container } = render( + + + + ); + await waitFor(() => { + expect(container).not.toBeEmptyDOMElement(); + }); + expect(screen.getByText('View proposals')).toBeInTheDocument(); + }); + + it('should render link to proposal', async () => { + const { container } = render( + + + + ); + await waitFor(() => { + expect(container).not.toBeEmptyDOMElement(); + }); + expect(screen.getByText('View proposal')).toBeInTheDocument(); + }); }); diff --git a/apps/trading/components/market-banner/market-termination-banner.tsx b/apps/trading/components/market-banner/market-termination-banner.tsx index 82ea89cd3..7f08bd2de 100644 --- a/apps/trading/components/market-banner/market-termination-banner.tsx +++ b/apps/trading/components/market-banner/market-termination-banner.tsx @@ -1,12 +1,67 @@ +import type { ReactNode } from 'react'; import { useState } from 'react'; import { format, formatDuration, intervalToDuration } from 'date-fns'; -import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit'; -import { useMarketViewProposals } from '@vegaprotocol/proposals'; +import { + ExternalLink, + Intent, + NotificationBanner, +} from '@vegaprotocol/ui-toolkit'; +import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; +import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals'; import { t } from '@vegaprotocol/i18n'; import * as Types from '@vegaprotocol/types'; import type { Market } from '@vegaprotocol/markets'; import { getQuoteName } from '@vegaprotocol/markets'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import sortBy from 'lodash/sortBy'; +import { + DApp, + TOKEN_PROPOSAL, + TOKEN_PROPOSALS, + useLinks, +} from '@vegaprotocol/environment'; +import { useDataProvider } from '@vegaprotocol/data-provider'; + +const filterProposals = ( + data: MarketViewProposalFieldsFragment[] | null, + marketId: string, + now: number +) => + sortBy( + (data || []).filter( + (item) => + item.terms.change.__typename === 'UpdateMarketState' && + item.terms.change.market.id === marketId && + item.terms.change.updateType === + Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE && + item.terms.enactmentDatetime && + new Date(item.terms.enactmentDatetime).getTime() > now + ), + (item) => item.terms.enactmentDatetime + ); + +const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => { + const enactmentDatetime = new Date(proposal.terms.enactmentDatetime); + const date = format(enactmentDatetime, 'dd MMMM'); + const duration = formatDuration( + intervalToDuration({ + start: new Date(), + end: enactmentDatetime, + }), + { + format: ['days', 'hours'], + } + ); + const price = + proposal.terms.change.__typename === 'UpdateMarketState' + ? proposal.terms.change.price + : ''; + return { + date, + duration, + price, + }; +}; export const MarketTerminationBanner = ({ market, @@ -15,57 +70,55 @@ export const MarketTerminationBanner = ({ }) => { const [visible, setVisible] = useState(true); const skip = !market || !visible; - const proposalsData = useMarketViewProposals({ + const { data: passedProposalsData } = useDataProvider({ + dataProvider: marketViewProposalsDataProvider, skip, - inState: Types.ProposalState.STATE_PASSED, - typename: 'UpdateMarketState', + variables: { + inState: Types.ProposalState.STATE_PASSED, + proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE, + }, }); + const { data: openProposalsData } = useDataProvider({ + dataProvider: marketViewProposalsDataProvider, + skip, + variables: { + inState: Types.ProposalState.STATE_OPEN, + proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE, + }, + }); + + const governanceLink = useLinks(DApp.Governance); + if (!market) return null; - const marketFound = (proposalsData || []).find( - (item) => - item.terms.change.__typename === 'UpdateMarketState' && - item.terms.change.market.id === market.id && - item.terms.change.updateType === - Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE && - item.state === Types.ProposalState.STATE_PASSED // subscription doesn't have state parameter - ); + const now = Date.now(); + const passedProposals = filterProposals(passedProposalsData, market.id, now); + const openProposals = filterProposals(openProposalsData, market.id, now); - const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime); - const name = - marketFound?.terms.change.__typename === 'UpdateMarketState' - ? marketFound.terms.change.market.tradableInstrument.instrument.code - : ''; + const name = market.tradableInstrument.instrument.code; + if (!passedProposals.length && !openProposals.length) { + return null; + } - if (name && enactmentDatetime.getTime() > Date.now()) { - const dayMonthDate = format(enactmentDatetime, 'dd MMMM'); - const duration = intervalToDuration({ - start: new Date(), - end: enactmentDatetime, - }); - const formattedDuration = formatDuration(duration, { - format: ['days', 'hours'], - }); - const price = - marketFound?.terms.change.__typename === 'UpdateMarketState' - ? marketFound.terms.change.price - : ''; - const assetSymbol = getQuoteName(market); - return ( - { - setVisible(false); - }} - data-testid={`termination-warning-banner-${market.id}`} - > + const assetSymbol = getQuoteName(market); + const proposalLink = + !passedProposals.length && openProposals[0]?.id + ? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id)) + : undefined; + const proposalsLink = + openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined; + let content: ReactNode; + if (passedProposals.length) { + const { date, duration, price } = getMessageVariables(passedProposals[0]); + content = ( + <>
- {t('Trading on Market %s will stop on %s', [name, dayMonthDate])} + {t('Trading on Market %s will stop on %s', [name, date])}
{t( 'You will no longer be able to hold a position on this market when it closes in %s.', - [formattedDuration] + [duration] )}{' '} {price && assetSymbol && @@ -74,8 +127,57 @@ export const MarketTerminationBanner = ({ assetSymbol, ])}
-
+ + ); + } else if (openProposals.length > 1) { + content = ( + <> +
+ {t( + 'Trading on Market %s may stop. There are open proposals to close this market', + [name] + )} +
+
+ + {t('View proposals')} + +
+ + ); + } else { + const { date, price } = getMessageVariables(openProposals[0]); + content = ( + <> +
+ {t( + 'Trading on Market %s may stop on %s. There is open proposal to close this market.', + [name, date] + )} +
+
+ {price && + assetSymbol && + t('Proposed final price is %s %s.', [ + addDecimalsFormatNumber(price, market.decimalPlaces), + assetSymbol, + ])} +
+
+ {t('View proposal')} +
+ ); } - return null; + return ( + { + setVisible(false); + }} + data-testid={`termination-warning-banner-${market.id}`} + > + {content} + + ); }; diff --git a/apps/trading/components/orders-container/orders-container.tsx b/apps/trading/components/orders-container/orders-container.tsx index 4145fba5f..0865b791d 100644 --- a/apps/trading/components/orders-container/orders-container.tsx +++ b/apps/trading/components/orders-container/orders-container.tsx @@ -10,6 +10,19 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice'; import { OrderStatus } from '@vegaprotocol/types'; import { Links } from '../../lib/links'; +const resolveNoRowsMessage = (filter?: Filter) => { + switch (filter) { + case Filter.Open: + return t('No open orders'); + case Filter.Closed: + return t('No closed orders'); + case Filter.Rejected: + return t('No rejected orders'); + default: + return t('No orders'); + } +}; + export const FilterStatusValue = { [Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED], [Filter.Closed]: [ @@ -43,6 +56,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => { if (!pubKey) { return {t('Please connect Vega wallet')}; } + const noRowsMessage = resolveNoRowsMessage(filter); return ( { } isReadOnly={isReadOnly} gridProps={gridStoreCallbacks} + noRowsMessage={noRowsMessage} /> ); }; diff --git a/libs/candles-chart/src/lib/candles-chart.tsx b/libs/candles-chart/src/lib/candles-chart.tsx index aeb1e4091..1cc76603a 100644 --- a/libs/candles-chart/src/lib/candles-chart.tsx +++ b/libs/candles-chart/src/lib/candles-chart.tsx @@ -63,7 +63,9 @@ export const CandlesChartContainer = ({ overlays, studies, notEnoughDataText: ( - {t('No data')} + + {t('No open orders')} + ), initialNumCandlesToDisplay: candlesCount, studySize: STUDY_SIZE, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx index 8984b64fc..32c9ec957 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx @@ -43,7 +43,7 @@ import { REDUCE_ONLY_TOOLTIP, stopSubmit, getNotionalSize, - getAssetUnit, + getBaseQuoteUnit, } from './deal-ticket'; import { TypeToggle } from './type-selector'; import { @@ -854,7 +854,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { }, [watch, market.id, updateStoredFormValues]); const quoteName = getQuoteName(market); - const assetUnit = getAssetUnit( + const assetUnit = getBaseQuoteUnit( market.tradableInstrument.instrument.metadata.tags ); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index bac4e87e9..0a71485d4 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -127,7 +127,7 @@ const getDefaultValues = ( ...storedValues, }); -export const getAssetUnit = (tags?: string[] | null) => +export const getBaseQuoteUnit = (tags?: string[] | null) => tags ?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:')) ?.replace(/^[^:]*:/, ''); @@ -273,9 +273,7 @@ export const DealTicket = ({ const assetSymbol = getAsset(market).symbol; - const assetUnit = getAssetUnit( - market.tradableInstrument.instrument.metadata.tags - ); + const assetUnit = getQuoteName(market); const summaryError = useMemo(() => { if (!pubKey) { diff --git a/libs/market-depth/src/lib/depth-chart.tsx b/libs/market-depth/src/lib/depth-chart.tsx index b77b951c9..b1712e119 100644 --- a/libs/market-depth/src/lib/depth-chart.tsx +++ b/libs/market-depth/src/lib/depth-chart.tsx @@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { volumeFormat={volumeFormat} priceFormat={priceFormat} notEnoughDataText={ - {t('No data')} + {t('No open orders')} } /> )} diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 54d0190b6..5975e9687 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -11,14 +11,8 @@ import type { MarketDepthQuery, MarketDepthQueryVariables, MarketDepthUpdateSubscription, - PriceLevelFieldsFragment, } from './__generated__/MarketDepth'; -export type OrderbookData = { - asks: PriceLevelFieldsFragment[]; - bids: PriceLevelFieldsFragment[]; -}; - interface OrderbookManagerProps { marketId: string; onClick: (args: { price?: string; size?: string }) => void; diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 0691487d3..8633e5b68 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -238,7 +238,7 @@ export const Orderbook = ({ ) : (
- {t('No data')} + {t('No open orders')}
)} diff --git a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx index 8716d46fe..593781c74 100644 --- a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx +++ b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx @@ -4,10 +4,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaTransactionStore } from '@vegaprotocol/web3'; import { useHasAmendableOrder } from '../../order-hooks'; -export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => { +export const OpenOrdersMenu = () => { const { isReadOnly } = useVegaWallet(); const create = useVegaTransactionStore((state) => state.create); - const hasAmendableOrder = useHasAmendableOrder(marketId); + const hasAmendableOrder = useHasAmendableOrder(); if (isReadOnly) { return null; diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts index d5f68bdb1..9c2602a76 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts @@ -239,11 +239,11 @@ export const hasActiveOrderProvider = makeDerivedDataProvider< export const hasAmendableOrderProvider = makeDerivedDataProvider< boolean, never, - { partyId: string; marketId?: string } + { partyId: string } >([activeOrdersProvider], (parts) => { const activeOrders = parts[0] as ReturnType; const hasAmendableOrder = activeOrders.some( - (order) => !(order.liquidityProvision || order.peggedOrder) + (order) => !order.liquidityProvision ); return hasAmendableOrder; }); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index fe8f1822a..214945252 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -1,23 +1,18 @@ -import { render, screen, act } from '@testing-library/react'; -import { OrderListManager } from './order-list-manager'; +import { render, screen } from '@testing-library/react'; +import type { OrderListManagerProps } from './order-list-manager'; +import { OrderListManager, Filter } from './order-list-manager'; import * as useDataProviderHook from '@vegaprotocol/data-provider'; import type { OrderFieldsFragment } from '../'; -import * as orderListMock from '../order-list/order-list'; -import { forwardRef } from 'react'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; -// @ts-ignore OrderList is read only but we need to override with the forwardRef to -// avoid warnings about padding refs -orderListMock.OrderListTable = forwardRef(() =>
OrderList
); - -const generateJsx = () => { +const generateJsx = (props: Partial | null = null) => { const pubKey = '0x123'; return ( - + ); @@ -33,9 +28,46 @@ describe('OrderListManager', () => { reload: jest.fn(), load: jest.fn(), }); - await act(async () => { - render(generateJsx()); + render(generateJsx()); + + expect( + await screen.getByRole('treegrid', { + name: (_, element) => element.classList.contains('ag-root'), + }) + ).toBeInTheDocument(); + }); + + it('should display order info', async () => { + jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ + data: [{ id: '1' } as OrderFieldsFragment], + loading: false, + error: new Error('Query error'), + flush: jest.fn(), + reload: jest.fn(), + load: jest.fn(), }); - expect(await screen.findByText('OrderList')).toBeInTheDocument(); + + render(generateJsx()); + + expect( + await screen.getByText('Something went wrong: Query error') + ).toBeInTheDocument(); + }); + + it('should display no rows overlay', async () => { + jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ + data: [], + loading: false, + error: undefined, + flush: jest.fn(), + reload: jest.fn(), + load: jest.fn(), + }); + + render( + generateJsx({ filter: Filter.Closed, noRowsMessage: 'no rows message' }) + ); + + expect(screen.getByText('no rows message')).toBeInTheDocument(); }); }); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index 2100934bf..bc682c68a 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -1,7 +1,8 @@ import { t } from '@vegaprotocol/i18n'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState, useEffect } from 'react'; import type { AgGridReact } from 'ag-grid-react'; -import { OrderListTable } from '../order-list/order-list'; +import type { FilterChangedEvent } from 'ag-grid-community'; +import { OrderListTable } from '../order-list'; import type { useDataGridEvents } from '@vegaprotocol/datagrid'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider'; @@ -11,6 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3'; import { OrderEditDialog } from '../order-list/order-edit-dialog'; import type { Order } from '../order-data-provider'; import { OrderViewDialog } from '../order-list/order-view-dialog'; +import { Splash } from '@vegaprotocol/ui-toolkit'; export enum Filter { 'Open' = 'Open', @@ -25,6 +27,7 @@ export interface OrderListManagerProps { isReadOnly: boolean; filter?: Filter; gridProps?: ReturnType; + noRowsMessage?: string; } export const OrderListManager = ({ @@ -34,6 +37,7 @@ export const OrderListManager = ({ isReadOnly, filter, gridProps, + noRowsMessage, }: OrderListManagerProps) => { const gridRef = useRef(null); const [editOrder, setEditOrder] = useState(null); @@ -56,6 +60,29 @@ export const OrderListManager = ({ }, }); + const onFilterChanged = useCallback( + (event: FilterChangedEvent) => { + gridProps?.onFilterChanged?.(event); + if (event.api) { + const isEmpty = event.api.getDisplayedRowCount() === 0; + if (isEmpty) { + event.api.showNoRowsOverlay(); + } else { + event.api.hideOverlay(); + } + } + }, + [gridProps] + ); + + useEffect(() => { + if (!data || !data.length) { + gridRef.current?.api?.showNoRowsOverlay(); + } else { + gridRef.current?.api?.hideOverlay(); + } + }, [data]); + const cancel = useCallback( (order: Order) => { if (!order.market) return; @@ -69,6 +96,10 @@ export const OrderListManager = ({ [create] ); + if (error) { + return {t(`Something went wrong: ${error.message}`)}; + } + return ( <>
@@ -82,8 +113,9 @@ export const OrderListManager = ({ onMarketClick={onMarketClick} onOrderTypeClick={onOrderTypeClick} isReadOnly={isReadOnly} - overlayNoRowsTemplate={error ? error.message : t('No orders')} + overlayNoRowsTemplate={noRowsMessage || t('No orders')} {...gridProps} + onFilterChanged={onFilterChanged} />
{editOrder && ( diff --git a/libs/orders/src/lib/components/order-list/order-edit-dialog.spec.tsx b/libs/orders/src/lib/components/order-list/order-edit-dialog.spec.tsx new file mode 100644 index 000000000..31a84ede1 --- /dev/null +++ b/libs/orders/src/lib/components/order-list/order-edit-dialog.spec.tsx @@ -0,0 +1,32 @@ +import { act, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { OrderEditDialog } from './order-edit-dialog'; +import { limitOrder } from '../mocks'; + +describe('OrderEditDialog', () => { + it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', async () => { + // 7003-MORD-013 + await act(async () => { + render( + + ); + }); + const editOrder = await screen.findByTestId('edit-order'); + const limitPrice = within(editOrder).getByLabelText('Price'); + await userEvent.type(limitPrice, '0.111111'); + const submitButton = within(editOrder).getByRole('button', { + name: 'Update', + }); + await userEvent.click(submitButton); + const inputErrorText = within(editOrder).getByTestId('input-error-text'); + expect(inputErrorText).toHaveTextContent( + 'Price accepts up to 1 decimal places' + ); + }); +}); diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index 5cfa49863..aa5078f5a 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -46,6 +46,12 @@ const generateJsx = ( describe('OrderListTable', () => { it('should render correct columns', async () => { + // 7003-MORD-001 + // 7003-MORD-002 + // 7003-MORD-003 + // 7003-MORD-004 + // 7003-MORD-005 + // 7003-MORD-006 await act(async () => { render(generateJsx({ rowData: [marketOrder, limitOrder] })); }); @@ -184,6 +190,7 @@ describe('OrderListTable', () => { describe('amend cell', () => { it('allows cancelling and editing for permitted orders', async () => { + // 7003-MORD-007 const mockEdit = jest.fn(); const mockCancel = jest.fn(); const order = generateOrder({ @@ -209,6 +216,7 @@ describe('OrderListTable', () => { }); it('does not allow cancelling and editing for permitted orders if read only', async () => { + // 7003-MORD-008 const mockEdit = jest.fn(); const mockCancel = jest.fn(); const order = generateOrder({ @@ -233,6 +241,7 @@ describe('OrderListTable', () => { }); it('shows if an order is a liquidity provision order and does not show order actions', async () => { + // 7003-MORD-008 const order = generateOrder({ type: Schema.OrderType.TYPE_LIMIT, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, @@ -251,6 +260,7 @@ describe('OrderListTable', () => { }); it('shows if an order is a pegged order and does not show order actions', async () => { + // 7003-MORD-008 const order = generateOrder({ type: Schema.OrderType.TYPE_LIMIT, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index 719e981d0..b952f36b8 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -34,7 +34,7 @@ import type { } from '@vegaprotocol/datagrid'; import type { AgGridReact } from 'ag-grid-react'; import type { Order } from '../order-data-provider'; -import { Filter } from '../order-list-manager'; +import { Filter } from '../order-list-manager/order-list-manager'; import type { ColDef } from 'ag-grid-community'; const defaultColDef = { diff --git a/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts b/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts index abc9359f5..321d61f34 100644 --- a/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts +++ b/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts @@ -3,7 +3,7 @@ import { hasAmendableOrderProvider } from '../components/order-data-provider'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { useDataProvider } from '@vegaprotocol/data-provider'; -export const useHasAmendableOrder = (marketId?: string) => { +export const useHasAmendableOrder = () => { const { pubKey } = useVegaWallet(); const [hasAmendableOrder, setHasAmendableOrder] = useState(false); const update = useCallback(({ data }: { data: boolean | null }) => { @@ -15,7 +15,6 @@ export const useHasAmendableOrder = (marketId?: string) => { update, variables: { partyId: pubKey || '', - marketId, }, skip: !pubKey, }); diff --git a/libs/proposals/src/lib/proposals-data-provider/index.ts b/libs/proposals/src/lib/proposals-data-provider/index.ts index 0c6a2a3b8..8b38a7f84 100644 --- a/libs/proposals/src/lib/proposals-data-provider/index.ts +++ b/libs/proposals/src/lib/proposals-data-provider/index.ts @@ -1,2 +1,5 @@ -export * from './proposals-data-provider'; +export { + marketViewProposalsDataProvider, + proposalsDataProvider, +} from './proposals-data-provider'; export * from './__generated__/Proposals'; diff --git a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.spec.tsx b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.spec.tsx new file mode 100644 index 000000000..c06281bc5 --- /dev/null +++ b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.spec.tsx @@ -0,0 +1,67 @@ +import { update } from './proposals-data-provider'; +import type { + MarketViewProposalFieldsFragment, + MarketViewProposalsQueryVariables, +} from './__generated__/Proposals'; +import * as Types from '@vegaprotocol/types'; + +describe('proposals data provider', () => { + const proposal = { + id: '1', + state: Types.ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarketState', + }, + }, + } as MarketViewProposalFieldsFragment; + const data = [proposal]; + const delta = { ...proposal }; + const reload = jest.fn(); + + const variables: MarketViewProposalsQueryVariables = { + inState: Types.ProposalState.STATE_OPEN, + proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE, + }; + + it('update existing data', () => { + expect(update(data, delta, reload, variables)?.[0]).toBe(delta); + }); + + it('removes existing data if delta do not match inState', () => { + expect( + update( + data, + { ...delta, state: Types.ProposalState.STATE_PASSED }, + reload, + variables + )?.length + ).toBe(0); + }); + + it('do not add delta if it do not match inState', () => { + expect( + update( + data, + { ...delta, id: '2', state: Types.ProposalState.STATE_PASSED }, + reload, + variables + )?.length + ).toBe(1); + }); + + it('do not add delta if it do not match proposalType', () => { + expect( + update( + data, + { + ...delta, + id: '2', + terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } }, + }, + reload, + variables + )?.length + ).toBe(1); + }); +}); diff --git a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx index 91bb0b846..31c13075e 100644 --- a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx +++ b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx @@ -1,5 +1,7 @@ +import type { Update } from '@vegaprotocol/data-provider'; import { makeDataProvider } from '@vegaprotocol/data-provider'; import produce from 'immer'; +import * as Types from '@vegaprotocol/types'; import type { ProposalsListQuery, ProposalsListQueryVariables, @@ -8,6 +10,7 @@ import type { MarketViewProposalFieldsFragment, MarketViewProposalsQuery, MarketViewProposalsQueryVariables, + MarketViewLiveProposalsSubscriptionVariables, } from './__generated__/Proposals'; import { MarketViewLiveProposalsDocument, @@ -42,21 +45,61 @@ export const proposalsDataProvider = makeDataProvider< errors.every((e) => e.message.match(/failed to get asset for ID/)), }); -const update = ( +const ProposalTypeMap: Record< + Types.ProposalType, + Types.ProposalChange['__typename'] +> = { + [Types.ProposalType.TYPE_CANCEL_TRANSFER]: 'CancelTransfer', + [Types.ProposalType.TYPE_NETWORK_PARAMETERS]: 'UpdateNetworkParameter', + [Types.ProposalType.TYPE_NEW_ASSET]: 'NewAsset', + [Types.ProposalType.TYPE_NEW_FREE_FORM]: 'NewFreeform', + [Types.ProposalType.TYPE_NEW_MARKET]: 'NewMarket', + [Types.ProposalType.TYPE_NEW_SPOT_MARKET]: 'NewSpotMarket', + [Types.ProposalType.TYPE_NEW_TRANSFER]: 'NewTransfer', + [Types.ProposalType.TYPE_UPDATE_ASSET]: 'UpdateAsset', + [Types.ProposalType.TYPE_UPDATE_MARKET]: 'UpdateMarket', + [Types.ProposalType.TYPE_UPDATE_MARKET_STATE]: 'UpdateMarketState', + [Types.ProposalType.TYPE_UPDATE_REFERRAL_PROGRAM]: 'UpdateReferralProgram', + [Types.ProposalType.TYPE_UPDATE_SPOT_MARKET]: 'UpdateSpotMarket', + [Types.ProposalType.TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM]: + 'UpdateVolumeDiscountProgram', +}; + +const matchFilter = ( + data: MarketViewProposalFieldsFragment, + variables: MarketViewProposalsQueryVariables +) => { + return ( + (!variables.inState || data.state === variables.inState) && + (!variables.proposalType || + data.terms.change.__typename === ProposalTypeMap[variables.proposalType]) + ); +}; + +export const update: Update< + MarketViewProposalFieldsFragment[] | null, + MarketViewProposalFieldsFragment, + MarketViewProposalsQueryVariables +> = ( data: MarketViewProposalFieldsFragment[] | null, - delta: MarketViewProposalFieldsFragment + delta: MarketViewProposalFieldsFragment, + reload, + variables ) => { const updateData = produce(data || [], (draft) => { const { id } = delta; const index = draft.findIndex((item) => item.id === id); + const match = matchFilter(delta, variables); if (index === -1) { - draft.unshift(delta); + if (match) { + draft.unshift(delta); + } } else { - const currNode = draft[index]; - draft[index] = { - ...currNode, - ...delta, - }; + if (match) { + draft[index] = delta; + } else { + draft.splice(index, 1); + } } }); return updateData; @@ -66,12 +109,15 @@ const getMarketProposalsData = ( responseData: MarketViewProposalsQuery | null ) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || []; +const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {}; + export const marketViewProposalsDataProvider = makeDataProvider< MarketViewProposalsQuery, MarketViewProposalFieldsFragment[], MarketViewLiveProposalsSubscription, MarketViewProposalFieldsFragment, - MarketViewProposalsQueryVariables + MarketViewProposalsQueryVariables, + MarketViewLiveProposalsSubscriptionVariables >({ query: MarketViewProposalsDocument, subscriptionQuery: MarketViewLiveProposalsDocument, @@ -79,4 +125,5 @@ export const marketViewProposalsDataProvider = makeDataProvider< getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) => subscriptionData.proposals, getData: getMarketProposalsData, + getSubscriptionVariables: () => subscriptionVariables, }); diff --git a/libs/proposals/src/lib/proposals-hooks/index.ts b/libs/proposals/src/lib/proposals-hooks/index.ts index fa469c55c..d61f740be 100644 --- a/libs/proposals/src/lib/proposals-hooks/index.ts +++ b/libs/proposals/src/lib/proposals-hooks/index.ts @@ -7,4 +7,3 @@ export * from './use-update-network-paramaters-toasts'; export * from './use-successor-market-proposal-details'; export * from './use-new-transfer-proposal-details'; export * from './use-cancel-transfer-proposal-details'; -export * from './use-market-view-proposals'; diff --git a/libs/proposals/src/lib/proposals-hooks/use-market-view-proposals.ts b/libs/proposals/src/lib/proposals-hooks/use-market-view-proposals.ts deleted file mode 100644 index b988add9d..000000000 --- a/libs/proposals/src/lib/proposals-hooks/use-market-view-proposals.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { marketViewProposalsDataProvider } from '../proposals-data-provider'; -import type * as Types from '@vegaprotocol/types'; - -export const useMarketViewProposals = ({ - skip = false, - typename, - proposalType, - inState, -}: { - typename: Types.ProposalChange['__typename']; - skip?: boolean; - inState: Types.ProposalState; - proposalType?: Types.ProposalType; -}) => { - const variables = { - inState, - ...(proposalType ? { proposalType } : null), - }; - - const { data } = useDataProvider({ - dataProvider: marketViewProposalsDataProvider, - skip, - variables, - }); - return (data || []).filter( - (item) => item.terms.change.__typename === typename - ); -}; diff --git a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts index e2ed7ff42..a59f8438d 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts +++ b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts @@ -1,9 +1,9 @@ -import { proposalsDataProvider } from '..'; import * as Schema from '@vegaprotocol/types'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { useMemo } from 'react'; import first from 'lodash/first'; -import type { ProposalListFieldsFragment } from '..'; +import { proposalsDataProvider } from '../proposals-data-provider'; +import type { ProposalListFieldsFragment } from '../proposals-data-provider'; type UseUpdateProposalProps = { id?: string; diff --git a/libs/react-helpers/src/hooks/use-theme-switcher.ts b/libs/react-helpers/src/hooks/use-theme-switcher.ts index 32882c91b..cc160dfe7 100644 --- a/libs/react-helpers/src/hooks/use-theme-switcher.ts +++ b/libs/react-helpers/src/hooks/use-theme-switcher.ts @@ -58,7 +58,7 @@ type ThemeStore = { setTheme: (theme?: Theme) => void; }; -const useThemeStore = create((set) => ({ +const useThemeStore = create()((set) => ({ theme: getCurrentTheme(), setTheme: (newTheme) => { set((state) => {