fix(orders): orders table flickering (#3105)

This commit is contained in:
Bartłomiej Głownia 2023-03-07 17:13:34 +01:00 committed by GitHub
parent ac163d0194
commit f10c33748f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 105 deletions

View File

@ -1,7 +1,7 @@
import { formatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Notification, Intent } from '@vegaprotocol/ui-toolkit';
import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits';
import { useDepositDialog } from '@vegaprotocol/deposits';
interface Props {
margin: string;
@ -16,25 +16,22 @@ interface Props {
export const MarginWarning = ({ margin, balance, asset }: Props) => {
const openDepositDialog = useDepositDialog((state) => state.open);
return (
<>
<Notification
intent={Intent.Warning}
testId="dealticket-warning-margin"
message={`You may not have enough margin available to open this position. ${formatNumber(
margin,
asset.decimals
)} ${asset.symbol} ${t(
'is currently required. You have only'
)} ${formatNumber(balance, asset.decimals)} ${asset.symbol} ${t(
'available.'
)}`}
buttonProps={{
text: t(`Deposit ${asset.symbol}`),
action: () => openDepositDialog(asset.id),
dataTestId: 'deal-ticket-deposit-dialog-button',
}}
/>
<DepositDialog />
</>
<Notification
intent={Intent.Warning}
testId="dealticket-warning-margin"
message={`You may not have enough margin available to open this position. ${formatNumber(
margin,
asset.decimals
)} ${asset.symbol} ${t(
'is currently required. You have only'
)} ${formatNumber(balance, asset.decimals)} ${asset.symbol} ${t(
'available.'
)}`}
buttonProps={{
text: t(`Deposit ${asset.symbol}`),
action: () => openDepositDialog(asset.id),
dataTestId: 'deal-ticket-deposit-dialog-button',
}}
/>
);
};

View File

@ -11,6 +11,7 @@ import {
import type { Market } from '@vegaprotocol/market-list';
import { marketsProvider } from '@vegaprotocol/market-list';
import type { PageInfo, Edge } from '@vegaprotocol/utils';
import { OrderStatus } from '@vegaprotocol/types';
import type {
OrderFieldsFragment,
OrderUpdateFieldsFragment,
@ -50,6 +51,9 @@ const orderMatchFilters = (
) {
return false;
}
if (variables?.filter?.excludeLiquidity && order.liquidityProvisionId) {
return false;
}
if (
variables?.dateRange?.start &&
!(
@ -177,3 +181,57 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
combineDelta<Order, ReturnType<typeof getDelta>['0']>,
combineInsertionData<Order>
);
const hasActiveOrderProviderInternal = makeDataProvider({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update: (
data: boolean | null,
delta: ReturnType<typeof getDelta>,
reload: () => void
) => {
const orders = delta?.filter(
(order) => !(order.peggedOrder || order.liquidityProvisionId)
);
if (!orders?.length) {
return data;
}
const hasActiveOrders = orders.some(
(order) => order.status === OrderStatus.STATUS_ACTIVE
);
if (hasActiveOrders) {
return true;
} else if (data && !hasActiveOrders) {
reload();
}
return data;
},
getData: (responseData: OrdersQuery | null) => {
const hasActiveOrder = !!responseData?.party?.ordersConnection?.edges?.some(
(order) => !(order.node.peggedOrder || order.node.liquidityProvision)
);
return hasActiveOrder;
},
getDelta,
});
export const hasActiveOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
hasActiveOrderProviderInternal(callback, client, {
filter: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
},
pagination: {
first: 1,
},
...variables,
} as OrdersQueryVariables),
],
(parts) => parts[0]
);

View File

@ -74,6 +74,27 @@ export const TransactionComplete = ({
);
};
const CancelAllOrdersButton = ({
onClick,
marketId,
}: {
onClick: (marketId?: string) => void;
marketId?: string;
}) => {
const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => onClick(marketId)}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
) : null;
};
export const OrderListManager = ({
partyId,
marketId,
@ -86,7 +107,6 @@ export const OrderListManager = ({
const [filter, setFilter] = useState<Filter | undefined>();
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows, reload } =
useOrderListData({
@ -140,7 +160,7 @@ export const OrderListManager = ({
[setSort]
);
const onCancel = useCallback(
const cancel = useCallback(
(order: Order) => {
if (!order.market) return;
create({
@ -153,6 +173,17 @@ export const OrderListManager = ({
[create]
);
const cancelAll = useCallback(
(marketId?: string) => {
create({
orderCancellation: {
marketId,
},
});
},
[create]
);
return (
<>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
@ -165,11 +196,10 @@ export const OrderListManager = ({
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={onCancel}
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
hasActiveOrder={hasActiveOrder}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
suppressNoRowsOverlay
@ -185,22 +215,8 @@ export const OrderListManager = ({
/>
</div>
</div>
{!isReadOnly && hasActiveOrder && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => {
create({
orderCancellation: {
marketId,
},
});
}}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
)}
</div>

View File

@ -31,11 +31,10 @@ export type OrderListTableProps = OrderListProps & {
setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
hasActiveOrder?: boolean;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrder, onMarketClick, hasActiveOrder, ...props }, ref) => {
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => {
return (
<AgGrid
ref={ref}
@ -47,7 +46,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
}}
style={{
width: '100%',
height: hasActiveOrder ? 'calc(100% - 46px)' : '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
{...props}

View File

@ -1,46 +1,24 @@
import { useState, useEffect, useMemo } from 'react';
import {
useOrdersUpdateSubscription,
useOrdersQuery,
} from '../components/order-data-provider/__generated__/Orders';
import { useCallback, useState } from 'react';
import { hasActiveOrderProvider } from '../components/order-data-provider/';
import { useVegaWallet } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/react-helpers';
export const useHasActiveOrder = (marketId?: string) => {
const { pubKey } = useVegaWallet();
const skip = !pubKey;
const [hasActiveOrder, setHasActiveOrder] = useState(false);
const subscriptionVariables = useMemo(
() => ({
const update = useCallback(({ data }: { data: boolean | null }) => {
console.log({ data });
setHasActiveOrder(!!data);
return true;
}, []);
useDataProvider({
dataProvider: hasActiveOrderProvider,
update,
variables: {
partyId: pubKey || '',
marketId,
}),
[pubKey, marketId]
);
const queryVariables = useMemo(
() => ({
...subscriptionVariables,
pagination: { first: 1 },
filter: { status: [Schema.OrderStatus.STATUS_ACTIVE] },
}),
[subscriptionVariables]
);
const { refetch, data, loading } = useOrdersQuery({
variables: queryVariables,
fetchPolicy: 'no-cache',
skip,
});
useEffect(() => {
if (!loading && data) {
setHasActiveOrder(!!data.party?.ordersConnection?.edges?.length);
}
}, [loading, data]);
useOrdersUpdateSubscription({
variables: subscriptionVariables,
onData: () => refetch(),
skip,
},
skip: !pubKey,
});
return hasActiveOrder;

View File

@ -62,8 +62,8 @@ describe('useDataProvider hook', () => {
});
expect(result.current.data).toEqual(updateCallbackPayload.data);
expect(result.current.loading).toEqual(false);
expect(update).toBeCalledTimes(1);
expect(update.mock.calls[0][0].data).toEqual(updateCallbackPayload.data);
expect(update).toBeCalledTimes(2);
expect(update.mock.calls[1][0].data).toEqual(updateCallbackPayload.data);
});
it('calls update on error', async () => {
@ -77,8 +77,8 @@ describe('useDataProvider hook', () => {
});
expect(result.current.data).toEqual(updateCallbackPayload.data);
expect(result.current.loading).toEqual(false);
expect(update).toBeCalledTimes(1);
expect(update.mock.calls[0][0].data).toEqual(updateCallbackPayload.data);
expect(update).toBeCalledTimes(2);
expect(update.mock.calls[1][0].data).toEqual(updateCallbackPayload.data);
});
it('calls update if isUpdate and skip setting state if update returns true', async () => {
@ -89,7 +89,8 @@ describe('useDataProvider hook', () => {
await act(async () => {
callback({ ...updateCallbackPayload, data: ++data });
});
expect(update).toBeCalledTimes(1);
expect(update).toBeCalledTimes(2);
expect(result.current.data).toEqual(data);
await act(async () => {
callback({
...updateCallbackPayload,
@ -99,9 +100,9 @@ describe('useDataProvider hook', () => {
});
});
expect(result.current.data).toEqual(data);
expect(update).toBeCalledTimes(2);
expect(update.mock.calls[1][0].data).toEqual(data);
expect(update.mock.calls[1][0].delta).toEqual(delta);
expect(update).toBeCalledTimes(3);
expect(update.mock.calls[2][0].data).toEqual(data);
expect(update.mock.calls[2][0].delta).toEqual(delta);
update.mockReturnValueOnce(true);
await act(async () => {
callback({
@ -111,10 +112,10 @@ describe('useDataProvider hook', () => {
isUpdate: true,
});
});
expect(result.current.data).toEqual(update.mock.calls[1][0].data);
expect(update).toBeCalledTimes(3);
expect(update.mock.calls[2][0].data).toEqual(data);
expect(update.mock.calls[2][0].delta).toEqual(delta);
expect(result.current.data).toEqual(update.mock.calls[2][0].data);
expect(update).toBeCalledTimes(4);
expect(update.mock.calls[3][0].data).toEqual(data);
expect(update.mock.calls[3][0].delta).toEqual(delta);
});
it('calls insert if isInsert and skip setting state if update returns true', async () => {
@ -159,7 +160,7 @@ describe('useDataProvider hook', () => {
await act(async () => {
callback({ ...updateCallbackPayload });
});
expect(update).toBeCalledTimes(1);
expect(update).toBeCalledTimes(2);
// setting same variables, with different object reference
await act(async () => {
@ -180,7 +181,7 @@ describe('useDataProvider hook', () => {
await act(async () => {
callback({ ...updateCallbackPayload });
});
expect(update).toBeCalledTimes(3);
expect(update).toBeCalledTimes(4);
// changing variables, apollo query will return error
await act(async () => {
@ -200,7 +201,7 @@ describe('useDataProvider hook', () => {
pageInfo: null,
});
});
expect(update).toBeCalledTimes(5);
expect(update).toBeCalledTimes(6);
});
it('do not create data provider instance when skip is true', async () => {

View File

@ -62,7 +62,6 @@ export const useDataProvider = <
const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
const initialized = useRef<boolean>(false);
const prevVariables = usePrevious(props.variables);
const [variables, setVariables] = useState(props.variables);
useEffect(() => {
@ -76,7 +75,6 @@ export const useDataProvider = <
}
}, []);
const reload = useCallback((force = false) => {
initialized.current = false;
if (reloadRef.current) {
reloadRef.current(force);
}
@ -118,11 +116,8 @@ export const useDataProvider = <
}
setTotalCount(totalCount);
setData(data);
if (!loading && !initialized.current) {
initialized.current = true;
if (update) {
update({ data });
}
if (!loading && !isUpdate && update) {
update({ data });
}
},
[update, insert, skipUpdates]
@ -131,10 +126,9 @@ export const useDataProvider = <
setData(null);
setError(undefined);
setTotalCount(undefined);
if (initialized.current && update) {
if (update) {
update({ data: null });
}
initialized.current = false;
if (skip) {
setLoading(false);
if (update) {
@ -157,7 +151,7 @@ export const useDataProvider = <
loadRef.current = undefined;
return unsubscribe();
};
}, [client, initialized, dataProvider, callback, variables, skip, update]);
}, [client, dataProvider, callback, variables, skip, update]);
return {
data,
loading,