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

View File

@ -11,6 +11,7 @@ import {
import type { Market } from '@vegaprotocol/market-list'; import type { Market } from '@vegaprotocol/market-list';
import { marketsProvider } from '@vegaprotocol/market-list'; import { marketsProvider } from '@vegaprotocol/market-list';
import type { PageInfo, Edge } from '@vegaprotocol/utils'; import type { PageInfo, Edge } from '@vegaprotocol/utils';
import { OrderStatus } from '@vegaprotocol/types';
import type { import type {
OrderFieldsFragment, OrderFieldsFragment,
OrderUpdateFieldsFragment, OrderUpdateFieldsFragment,
@ -50,6 +51,9 @@ const orderMatchFilters = (
) { ) {
return false; return false;
} }
if (variables?.filter?.excludeLiquidity && order.liquidityProvisionId) {
return false;
}
if ( if (
variables?.dateRange?.start && variables?.dateRange?.start &&
!( !(
@ -177,3 +181,57 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
combineDelta<Order, ReturnType<typeof getDelta>['0']>, combineDelta<Order, ReturnType<typeof getDelta>['0']>,
combineInsertionData<Order> 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 = ({ export const OrderListManager = ({
partyId, partyId,
marketId, marketId,
@ -86,7 +107,6 @@ export const OrderListManager = ({
const [filter, setFilter] = useState<Filter | undefined>(); const [filter, setFilter] = useState<Filter | undefined>();
const [editOrder, setEditOrder] = useState<Order | null>(null); const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows, reload } = const { data, error, loading, addNewRows, getRows, reload } =
useOrderListData({ useOrderListData({
@ -140,7 +160,7 @@ export const OrderListManager = ({
[setSort] [setSort]
); );
const onCancel = useCallback( const cancel = useCallback(
(order: Order) => { (order: Order) => {
if (!order.market) return; if (!order.market) return;
create({ create({
@ -153,6 +173,17 @@ export const OrderListManager = ({
[create] [create]
); );
const cancelAll = useCallback(
(marketId?: string) => {
create({
orderCancellation: {
marketId,
},
});
},
[create]
);
return ( return (
<> <>
<div className="h-full relative grid grid-rows-[1fr,min-content]"> <div className="h-full relative grid grid-rows-[1fr,min-content]">
@ -165,11 +196,10 @@ export const OrderListManager = ({
onBodyScroll={onBodyScroll} onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged} onFilterChanged={onFilterChanged}
onSortChanged={onSortChange} onSortChanged={onSortChange}
cancel={onCancel} cancel={cancel}
setEditOrder={setEditOrder} setEditOrder={setEditOrder}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
hasActiveOrder={hasActiveOrder}
blockLoadDebounceMillis={100} blockLoadDebounceMillis={100}
suppressLoadingOverlay suppressLoadingOverlay
suppressNoRowsOverlay suppressNoRowsOverlay
@ -185,22 +215,8 @@ export const OrderListManager = ({
/> />
</div> </div>
</div> </div>
{!isReadOnly && hasActiveOrder && ( {!isReadOnly && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2"> <CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
<Button
size="sm"
onClick={() => {
create({
orderCancellation: {
marketId,
},
});
}}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
)} )}
</div> </div>

View File

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

View File

@ -1,46 +1,24 @@
import { useState, useEffect, useMemo } from 'react'; import { useCallback, useState } from 'react';
import { import { hasActiveOrderProvider } from '../components/order-data-provider/';
useOrdersUpdateSubscription,
useOrdersQuery,
} from '../components/order-data-provider/__generated__/Orders';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types'; import { useDataProvider } from '@vegaprotocol/react-helpers';
export const useHasActiveOrder = (marketId?: string) => { export const useHasActiveOrder = (marketId?: string) => {
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const skip = !pubKey;
const [hasActiveOrder, setHasActiveOrder] = useState(false); 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 || '', partyId: pubKey || '',
marketId, marketId,
}), },
[pubKey, marketId] skip: !pubKey,
);
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,
}); });
return hasActiveOrder; return hasActiveOrder;

View File

@ -62,8 +62,8 @@ describe('useDataProvider hook', () => {
}); });
expect(result.current.data).toEqual(updateCallbackPayload.data); expect(result.current.data).toEqual(updateCallbackPayload.data);
expect(result.current.loading).toEqual(false); expect(result.current.loading).toEqual(false);
expect(update).toBeCalledTimes(1); expect(update).toBeCalledTimes(2);
expect(update.mock.calls[0][0].data).toEqual(updateCallbackPayload.data); expect(update.mock.calls[1][0].data).toEqual(updateCallbackPayload.data);
}); });
it('calls update on error', async () => { it('calls update on error', async () => {
@ -77,8 +77,8 @@ describe('useDataProvider hook', () => {
}); });
expect(result.current.data).toEqual(updateCallbackPayload.data); expect(result.current.data).toEqual(updateCallbackPayload.data);
expect(result.current.loading).toEqual(false); expect(result.current.loading).toEqual(false);
expect(update).toBeCalledTimes(1); expect(update).toBeCalledTimes(2);
expect(update.mock.calls[0][0].data).toEqual(updateCallbackPayload.data); expect(update.mock.calls[1][0].data).toEqual(updateCallbackPayload.data);
}); });
it('calls update if isUpdate and skip setting state if update returns true', async () => { it('calls update if isUpdate and skip setting state if update returns true', async () => {
@ -89,7 +89,8 @@ describe('useDataProvider hook', () => {
await act(async () => { await act(async () => {
callback({ ...updateCallbackPayload, data: ++data }); callback({ ...updateCallbackPayload, data: ++data });
}); });
expect(update).toBeCalledTimes(1); expect(update).toBeCalledTimes(2);
expect(result.current.data).toEqual(data);
await act(async () => { await act(async () => {
callback({ callback({
...updateCallbackPayload, ...updateCallbackPayload,
@ -99,9 +100,9 @@ describe('useDataProvider hook', () => {
}); });
}); });
expect(result.current.data).toEqual(data); expect(result.current.data).toEqual(data);
expect(update).toBeCalledTimes(2); expect(update).toBeCalledTimes(3);
expect(update.mock.calls[1][0].data).toEqual(data); expect(update.mock.calls[2][0].data).toEqual(data);
expect(update.mock.calls[1][0].delta).toEqual(delta); expect(update.mock.calls[2][0].delta).toEqual(delta);
update.mockReturnValueOnce(true); update.mockReturnValueOnce(true);
await act(async () => { await act(async () => {
callback({ callback({
@ -111,10 +112,10 @@ describe('useDataProvider hook', () => {
isUpdate: true, isUpdate: true,
}); });
}); });
expect(result.current.data).toEqual(update.mock.calls[1][0].data); expect(result.current.data).toEqual(update.mock.calls[2][0].data);
expect(update).toBeCalledTimes(3); expect(update).toBeCalledTimes(4);
expect(update.mock.calls[2][0].data).toEqual(data); expect(update.mock.calls[3][0].data).toEqual(data);
expect(update.mock.calls[2][0].delta).toEqual(delta); expect(update.mock.calls[3][0].delta).toEqual(delta);
}); });
it('calls insert if isInsert and skip setting state if update returns true', async () => { it('calls insert if isInsert and skip setting state if update returns true', async () => {
@ -159,7 +160,7 @@ describe('useDataProvider hook', () => {
await act(async () => { await act(async () => {
callback({ ...updateCallbackPayload }); callback({ ...updateCallbackPayload });
}); });
expect(update).toBeCalledTimes(1); expect(update).toBeCalledTimes(2);
// setting same variables, with different object reference // setting same variables, with different object reference
await act(async () => { await act(async () => {
@ -180,7 +181,7 @@ describe('useDataProvider hook', () => {
await act(async () => { await act(async () => {
callback({ ...updateCallbackPayload }); callback({ ...updateCallbackPayload });
}); });
expect(update).toBeCalledTimes(3); expect(update).toBeCalledTimes(4);
// changing variables, apollo query will return error // changing variables, apollo query will return error
await act(async () => { await act(async () => {
@ -200,7 +201,7 @@ describe('useDataProvider hook', () => {
pageInfo: null, pageInfo: null,
}); });
}); });
expect(update).toBeCalledTimes(5); expect(update).toBeCalledTimes(6);
}); });
it('do not create data provider instance when skip is true', async () => { 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 flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined); const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined); const loadRef = useRef<Load<Data> | undefined>(undefined);
const initialized = useRef<boolean>(false);
const prevVariables = usePrevious(props.variables); const prevVariables = usePrevious(props.variables);
const [variables, setVariables] = useState(props.variables); const [variables, setVariables] = useState(props.variables);
useEffect(() => { useEffect(() => {
@ -76,7 +75,6 @@ export const useDataProvider = <
} }
}, []); }, []);
const reload = useCallback((force = false) => { const reload = useCallback((force = false) => {
initialized.current = false;
if (reloadRef.current) { if (reloadRef.current) {
reloadRef.current(force); reloadRef.current(force);
} }
@ -118,11 +116,8 @@ export const useDataProvider = <
} }
setTotalCount(totalCount); setTotalCount(totalCount);
setData(data); setData(data);
if (!loading && !initialized.current) { if (!loading && !isUpdate && update) {
initialized.current = true; update({ data });
if (update) {
update({ data });
}
} }
}, },
[update, insert, skipUpdates] [update, insert, skipUpdates]
@ -131,10 +126,9 @@ export const useDataProvider = <
setData(null); setData(null);
setError(undefined); setError(undefined);
setTotalCount(undefined); setTotalCount(undefined);
if (initialized.current && update) { if (update) {
update({ data: null }); update({ data: null });
} }
initialized.current = false;
if (skip) { if (skip) {
setLoading(false); setLoading(false);
if (update) { if (update) {
@ -157,7 +151,7 @@ export const useDataProvider = <
loadRef.current = undefined; loadRef.current = undefined;
return unsubscribe(); return unsubscribe();
}; };
}, [client, initialized, dataProvider, callback, variables, skip, update]); }, [client, dataProvider, callback, variables, skip, update]);
return { return {
data, data,
loading, loading,