fix: order warning messages (#1037)

* fix: order validation updates & warning messages

* fix: add extra warning message

* fix: order list and test

* fix: text-ui on loading market data...

* fix:  when market is cancelled it can't accept orders

* fix: display rejection reason behind stopped orders

* fix: remove punctuation marks from warning/reasoning message

* fix: format order-feedback

* fix: order feedback test

* fix: do not use market state to display

* fix: format use order valid hook

* fix: add required and min price on edit and deal ticket

* fix: remove price validation on market orders as there is no price input

* fix: check format

* fix: format error labels

* fix: order validation test

* Update package.json with test:all

* Update libs/orders/src/lib/components/order-feedback/order-feedback.tsx

Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>

* fix: add step on input

* fix: remove decimal places format on init

* fix: input type fix

* fix: order edit price with toDecimals stepper

* fix: remove set value and null check in form

* fix: use form validate on edit

* fix: try fixing test after step added

* fix: making rejection reason startcase again

Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
This commit is contained in:
m.ray 2022-08-18 14:53:24 +02:00 committed by GitHub
parent ba3460496d
commit dbb21a4745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 141 additions and 182 deletions

View File

@ -60,6 +60,14 @@ export const getOrderDialogTitle = (
return t('Order partially filled');
case OrderStatus.Parked:
return t('Order parked');
case OrderStatus.Stopped:
return t('Order stopped');
case OrderStatus.Cancelled:
return t('Order cancelled');
case OrderStatus.Expired:
return t('Order expired');
case OrderStatus.Rejected:
return t('Order rejected');
default:
return t('Submission failed');
}
@ -71,15 +79,18 @@ export const getOrderDialogIntent = (
if (!status) {
return;
}
switch (status) {
case OrderStatus.Parked:
case OrderStatus.Expired:
case OrderStatus.PartiallyFilled:
return Intent.Warning;
case OrderStatus.Rejected:
case OrderStatus.Stopped:
case OrderStatus.Cancelled:
return Intent.Danger;
case OrderStatus.Filled:
case OrderStatus.Active:
return Intent.Success;
default:
return;
}

View File

@ -132,6 +132,7 @@ export const DealTicket = ({
</Button>
{message && (
<InputError
intent={isDisabled ? 'danger' : 'warning'}
className="mt-12 mb-12"
data-testid="dealticket-error-message"
>

View File

@ -100,7 +100,7 @@ export const SelectAllMarketsTableBody = ({
) : (
<thead>
<tr>
<td className="text-black dark:text-white text-h5">
<td className="text-black dark:text-white text-ui">
{t('Loading market data...')}
</td>
</tr>

View File

@ -1,5 +1,4 @@
import { render, screen } from '@testing-library/react';
import { formatLabel } from '@vegaprotocol/react-helpers';
import {
OrderRejectionReason,
OrderStatus,
@ -7,6 +6,7 @@ import {
Side,
} from '@vegaprotocol/types';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import startCase from 'lodash/startCase';
import { generateOrder } from '../mocks/generate-orders';
import type { OrderFeedbackProps } from './order-feedback';
import { OrderFeedback } from './order-feedback';
@ -45,7 +45,7 @@ describe('OrderFeedback', () => {
const order = generateOrder(orderFields);
render(<OrderFeedback {...props} order={order} />);
expect(screen.getByTestId('error-reason')).toHaveTextContent(
`Reason: ${formatLabel(orderFields.rejectionReason)}`
`${startCase(orderFields.rejectionReason)}`
);
});

View File

@ -1,12 +1,9 @@
import { useEnvironment } from '@vegaprotocol/environment';
import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__';
import {
addDecimalsFormatNumber,
formatLabel,
t,
} from '@vegaprotocol/react-helpers';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { OrderStatus, OrderType, Side } from '@vegaprotocol/types';
import type { VegaTxState } from '@vegaprotocol/wallet';
import startCase from 'lodash/startCase';
export interface OrderFeedbackProps {
transaction: VegaTxState;
@ -18,46 +15,7 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
const labelClass = 'font-bold text-black dark:text-white';
if (!order) return null;
// Order on network but was rejected
if (order.status === OrderStatus.Rejected) {
return (
<p data-testid="error-reason">
{order.rejectionReason &&
t(`Reason: ${formatLabel(order.rejectionReason)}`)}
</p>
);
}
if (order.status === OrderStatus.Cancelled) {
return (
<div data-testid="order-confirmed">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
{order.market && (
<div>
<p className={labelClass}>{t(`Market`)}</p>
<p>{t(`${order.market.name}`)}</p>
</div>
)}
</div>
<div>
{transaction.txHash && (
<div>
<p className={labelClass}>{t('Transaction')}</p>
<a
className="underline break-words"
data-testid="tx-block-explorer"
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
target="_blank"
rel="noreferrer"
>
{transaction.txHash}
</a>
</div>
)}
</div>
</div>
);
}
const orderRejectionReason = getRejectionReason(order);
return (
<div data-testid="order-confirmed">
@ -113,6 +71,27 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
</div>
)}
</div>
{orderRejectionReason && (
<div>
<p className={labelClass}>{t(`Reason`)}</p>
<p data-testid="error-reason">{t(orderRejectionReason)}</p>
</div>
)}
</div>
);
};
const getRejectionReason = (
order: OrderEvent_busEvents_event_Order
): string | null => {
switch (order.status) {
case OrderStatus.Stopped:
return t(
`Your ${order.timeInForce} order was not filled and it has been stopped`
);
case OrderStatus.Rejected:
return order.rejectionReason && t(startCase(order.rejectionReason));
default:
return null;
}
};

View File

@ -1,7 +1,7 @@
import {
addDecimal,
t,
addDecimalsFormatNumber,
toDecimal,
} from '@vegaprotocol/react-helpers';
import { OrderType } from '@vegaprotocol/types';
import {
@ -18,7 +18,7 @@ import type { OrderFields } from '../order-data-provider';
interface OrderEditDialogProps {
isOpen: boolean;
onChange: (isOpen: boolean) => void;
order: OrderFields | null;
order: OrderFields;
onSubmit: (fields: FormFields) => void;
}
@ -37,15 +37,9 @@ export const OrderEditDialog = ({
register,
formState: { errors },
handleSubmit,
} = useForm<FormFields>({
defaultValues: {
entryPrice: order?.price
? addDecimal(order?.price, order?.market?.decimalPlaces ?? 0)
: '',
},
});
} = useForm<FormFields>();
if (!order) return null;
const step = toDecimal(order.market?.decimalPlaces ?? 0);
return (
<Dialog
@ -88,9 +82,18 @@ export const OrderEditDialog = ({
<form onSubmit={handleSubmit(onSubmit)} data-testid="edit-order">
<FormGroup label={t('Entry price')} labelFor="entryPrice">
<Input
{...register('entryPrice', { required: t('Required') })}
type="number"
step={step}
{...register('entryPrice', {
required: t('You need to provide a price'),
validate: {
min: (value) =>
Number(value) > 0
? true
: t('The price cannot be negative'),
},
})}
id="entryPrice"
type="text"
/>
{errors.entryPrice?.message && (
<InputError intent="danger" className="mt-4">

View File

@ -1,9 +1,5 @@
import { act, render, screen } from '@testing-library/react';
import {
addDecimal,
formatLabel,
getDateTimeFormat,
} from '@vegaprotocol/react-helpers';
import { addDecimal, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
@ -13,6 +9,7 @@ import { MockedProvider } from '@apollo/client/testing';
import { OrderListTable } from '../';
import type { Orders_party_ordersConnection_edges_node } from '../';
import { limitOrder, marketOrder } from '../mocks/generate-orders';
import startCase from 'lodash/startCase';
const generateJsx = (
orders: Orders_party_ordersConnection_edges_node[] | null,
@ -122,7 +119,7 @@ describe('OrderListTable', () => {
});
const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent(
`${rejectedOrder.status}: ${formatLabel(rejectedOrder.rejectionReason)}`
`${rejectedOrder.status}: ${startCase(rejectedOrder.rejectionReason)}`
);
});
});

View File

@ -29,7 +29,7 @@ const Template: Story = (args) => {
const Template2: Story = (args) => {
const [open, setOpen] = useState(false);
const [editOrder, setEditOrder] = useState<OrderFields | null>(null);
const [editOrder, setEditOrder] = useState<OrderFields>();
const cancel = () => {
setOpen(!open);
return Promise.resolve();
@ -57,16 +57,18 @@ const Template2: Story = (args) => {
onChange={setOpen}
transaction={transaction}
/>
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
return;
}}
/>
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(undefined);
}}
order={editOrder}
onSubmit={(fields) => {
return;
}}
/>
)}
</>
);
};

View File

@ -1,10 +1,5 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import {
addDecimal,
formatLabel,
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers';
import {
AgGridDynamic as AgGrid,
Button,
@ -29,6 +24,7 @@ import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderEditDialog } from './order-edit-dialog';
import type { OrderFields } from '../order-data-provider/__generated__';
import { OrderFeedback } from '../order-feedback';
import startCase from 'lodash/startCase';
type OrderListProps = AgGridReactProps | AgReactUiProps;
@ -69,17 +65,19 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
order={orderEdit.updatedOrder}
/>
</orderEdit.TransactionDialog>
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
setEditOrder(null);
orderEdit.edit({ price: fields.entryPrice });
}}
/>
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
setEditOrder(null);
orderEdit.edit({ price: fields.entryPrice });
}}
/>
)}
</>
);
}
@ -158,7 +156,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
}
if (value === OrderStatus.Rejected) {
return `${value}: ${
data.rejectionReason && formatLabel(data.rejectionReason)
data.rejectionReason && startCase(data.rejectionReason)
}`;
}
return value;
@ -316,8 +314,16 @@ const getEditDialogTitle = (status?: OrderStatus): string | undefined => {
return t('Order partially filled');
case OrderStatus.Parked:
return t('Order parked');
case OrderStatus.Stopped:
return t('Order stopped');
case OrderStatus.Expired:
return t('Order expired');
case OrderStatus.Cancelled:
return t('Order cancelled');
case OrderStatus.Rejected:
return t('Order rejected');
default:
return t('Submission failed');
return t('Order amendment failed');
}
};

View File

@ -1,5 +1,4 @@
import { act, renderHook } from '@testing-library/react';
import type { Order } from '../utils';
import type {
VegaKeyExtended,
VegaWalletContextShape,
@ -12,6 +11,7 @@ import {
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { Order } from './use-order-submit';
import { useOrderSubmit } from './use-order-submit';
import type {
OrderEvent,
@ -20,7 +20,6 @@ import type {
import { ORDER_EVENT_SUB } from './order-event-query';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { Market } from '../market';
import { toNanoSeconds } from '@vegaprotocol/react-helpers';
const defaultMarket = {
@ -47,7 +46,7 @@ const defaultMarket = {
price: '100',
},
},
} as Market;
};
const defaultWalletContext = {
keypair: null,
@ -158,7 +157,7 @@ describe('useOrderSubmit', () => {
keypair,
});
const order: Order = {
const order = {
type: VegaWalletOrderType.Limit,
size: '10',
timeInForce: VegaWalletOrderTimeInForce.GTT,

View File

@ -13,11 +13,10 @@ import type { ValidationProps } from './use-order-validation';
import { marketTranslations } from './use-order-validation';
import { useOrderValidation } from './use-order-validation';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { Market } from '../market';
jest.mock('@vegaprotocol/wallet');
const market: Market = {
const market = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
@ -114,6 +113,8 @@ describe('useOrderValidation', () => {
${MarketState.Settled}
${MarketState.Rejected}
${MarketState.TradingTerminated}
${MarketState.Closed}
${MarketState.Cancelled}
`(
'Returns an error message for market state when not accepting orders',
({ state }) => {
@ -129,11 +130,8 @@ describe('useOrderValidation', () => {
it.each`
state
${MarketState.Suspended}
${MarketState.Pending}
${MarketState.Cancelled}
${MarketState.Proposed}
${MarketState.Closed}
`(
'Returns an error message for market state suspended or pending',
({ state }) => {
@ -207,6 +205,7 @@ describe('useOrderValidation', () => {
({ fieldName, errorType, errorMessage }) => {
const { result } = setup({
fieldErrors: { [fieldName]: { type: errorType } },
orderType: VegaWalletOrderType.Limit,
});
expect(result.current).toStrictEqual({
isDisabled: true,

View File

@ -10,7 +10,8 @@ import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { Order } from './use-order-submit';
export type ValidationArgs = {
export type ValidationProps = {
step?: number;
market: {
state: MarketState;
tradingMode: MarketTradingMode;
@ -35,7 +36,7 @@ export const useOrderValidation = ({
fieldErrors = {},
orderType,
orderTimeInForce,
}: ValidationArgs) => {
}: ValidationProps) => {
const { keypair } = useVegaWallet();
const minSize = toDecimal(market.positionDecimalPlaces);
@ -56,6 +57,8 @@ export const useOrderValidation = ({
MarketState.Settled,
MarketState.Rejected,
MarketState.TradingTerminated,
MarketState.Cancelled,
MarketState.Closed,
].includes(market.state)
) {
return {
@ -68,15 +71,7 @@ export const useOrderValidation = ({
};
}
if (
[
MarketState.Suspended,
MarketState.Pending,
MarketState.Proposed,
MarketState.Cancelled,
MarketState.Closed,
].includes(market.state)
) {
if ([MarketState.Proposed, MarketState.Pending].includes(market.state)) {
return {
isDisabled: false,
message: t(
@ -87,65 +82,13 @@ export const useOrderValidation = ({
};
}
if (market.state !== MarketState.Active) {
if (market.state === MarketState.Suspended) {
if (market.tradingMode === MarketTradingMode.Continuous) {
if (orderType !== OrderType.Limit) {
return {
isDisabled: true,
message: t(
'Only limit orders are permitted when market is in auction'
),
};
}
if (
[
OrderTimeInForce.FOK,
OrderTimeInForce.IOC,
OrderTimeInForce.GFN,
].includes(orderTimeInForce)
) {
return {
isDisabled: true,
message: t(
'Only GTT, GTC and GFA are permitted when market is in auction'
),
};
}
}
return {
isDisabled: false,
message: t(
`This market is ${marketTranslations(
market.state
)} and only accepting liquidity commitment orders`
),
};
}
if (
market.state === MarketState.Proposed ||
market.state === MarketState.Pending
) {
return {
isDisabled: false,
message: t(
`This market is ${marketTranslations(
market.state
)} and only accepting liquidity commitment orders`
),
};
}
return {
isDisabled: true,
message: t('This market is no longer active.'),
};
}
if (market.tradingMode !== MarketTradingMode.Continuous) {
if (
[
MarketTradingMode.BatchAuction,
MarketTradingMode.MonitoringAuction,
MarketTradingMode.OpeningAuction,
].includes(market.tradingMode)
) {
if (orderType !== OrderType.Limit) {
return {
isDisabled: true,
@ -185,14 +128,17 @@ export const useOrderValidation = ({
};
}
if (fieldErrors?.price?.type === 'required') {
if (
fieldErrors?.price?.type === 'required' &&
orderType !== OrderType.Market
) {
return {
isDisabled: true,
message: t('You need to provide a price'),
};
}
if (fieldErrors?.price?.type === 'min') {
if (fieldErrors?.price?.type === 'min' && orderType !== OrderType.Market) {
return {
isDisabled: true,
message: t(`The price cannot be negative`),
@ -217,6 +163,21 @@ export const useOrderValidation = ({
};
}
if (
[
MarketTradingMode.BatchAuction,
MarketTradingMode.MonitoringAuction,
MarketTradingMode.OpeningAuction,
].includes(market.tradingMode)
) {
return {
isDisabled: false,
message: t(
'Any orders placed now will not trade until the auction ends'
),
};
}
return { isDisabled: false, message: '' };
}, [
minSize,

View File

@ -4,7 +4,7 @@ import memoize from 'lodash/memoize';
import { getUserLocale } from './utils';
export function toDecimal(numberOfDecimals: number) {
return Math.pow(10, -numberOfDecimals);
return 1 / Math.pow(10, numberOfDecimals);
}
export function toBigNum(

View File

@ -1,5 +1,5 @@
import { useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/react-helpers';
import { formatLabel, t } from '@vegaprotocol/react-helpers';
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { VegaTxState } from '../use-vega-transaction';
@ -72,7 +72,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
if (transaction.status === VegaTxStatus.Error) {
return (
<div data-testid={transaction.status}>
<p>{transaction.error}</p>
<p>{transaction.error && formatLabel(transaction.error)}</p>
</div>
);
}

View File

@ -6,7 +6,8 @@
"start": "nx serve",
"build": "nx build",
"test": "nx test",
"postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack"
"postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack",
"test:all": "nx run-many --all --target=test"
},
"engines": {
"node": ">=16.14.0"