Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8990c1185c | ||
|
|
375f9b07ea |
@@ -1,42 +1,23 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import compact from 'lodash/compact';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community';
|
||||
|
||||
import { OrderListTable } from '../order-list/order-list';
|
||||
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
|
||||
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
||||
import {
|
||||
normalizeOrderAmendment,
|
||||
useVegaTransactionStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||
import { useMarketsQuery } from '@vegaprotocol/markets';
|
||||
import { useAssetsQuery } from '@vegaprotocol/assets';
|
||||
import { AgGridLazy } from '@vegaprotocol/datagrid';
|
||||
import type { OrdersUpdateSubscription } from '../order-data-provider';
|
||||
import { useOrdersUpdateSubscription } from '../order-data-provider';
|
||||
import type { ArrayElement } from 'type-fest/source/internal';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { orderBy, uniqBy } from 'lodash';
|
||||
import type { ColDef, GridApi } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
|
||||
export enum Filter {
|
||||
'Open',
|
||||
'Closed',
|
||||
'Rejected',
|
||||
}
|
||||
|
||||
const FilterStatusValue = {
|
||||
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
|
||||
[Filter.Closed]: [
|
||||
OrderStatus.STATUS_CANCELLED,
|
||||
OrderStatus.STATUS_EXPIRED,
|
||||
OrderStatus.STATUS_FILLED,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
OrderStatus.STATUS_STOPPED,
|
||||
],
|
||||
[Filter.Rejected]: [OrderStatus.STATUS_REJECTED],
|
||||
};
|
||||
|
||||
export interface OrderListManagerProps {
|
||||
partyId: string;
|
||||
marketId?: string;
|
||||
@@ -48,164 +29,209 @@ export interface OrderListManagerProps {
|
||||
storeKey?: string;
|
||||
}
|
||||
|
||||
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
data-testid="cancelAll"
|
||||
>
|
||||
{t('Cancel all')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
const useMarkets = () => {
|
||||
const { data, loading, error } = useMarketsQuery();
|
||||
|
||||
export const OrderListManager = ({
|
||||
partyId,
|
||||
marketId,
|
||||
onMarketClick,
|
||||
onOrderTypeClick,
|
||||
isReadOnly,
|
||||
enforceBottomPlaceholder,
|
||||
filter,
|
||||
storeKey,
|
||||
}: OrderListManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [hasData, setHasData] = useState(false);
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const hasAmendableOrder = useHasAmendableOrder(marketId);
|
||||
const { data, error, loading, reload } = useDataProvider({
|
||||
dataProvider: ordersWithMarketProvider,
|
||||
variables:
|
||||
filter === Filter.Open
|
||||
? { partyId, filter: { liveOnly: true } }
|
||||
: { partyId },
|
||||
const markets = data?.marketsConnection?.edges.map((e) => e.node) || [];
|
||||
const marketLookup: Record<string, MarketFieldsFragment> = {};
|
||||
|
||||
markets.forEach((m) => {
|
||||
marketLookup[m.id] = m;
|
||||
});
|
||||
|
||||
const {
|
||||
onFilterChanged: bottomPlaceholderOnFilterChanged,
|
||||
...bottomPlaceholderProps
|
||||
} = useBottomPlaceholder({
|
||||
gridRef,
|
||||
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
|
||||
});
|
||||
return {
|
||||
markets: marketLookup,
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const cancel = useCallback(
|
||||
(order: Order) => {
|
||||
if (!order.market) return;
|
||||
create({
|
||||
orderCancellation: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
},
|
||||
const useAssets = () => {
|
||||
const { data, loading, error } = useAssetsQuery();
|
||||
return {
|
||||
assets: compact(data?.assetsConnection?.edges || []).map((e) => e.node),
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
type Order = ArrayElement<OrdersUpdateSubscription['orders']>;
|
||||
|
||||
const useOrders = (partyId?: string) => {
|
||||
const loadRef = useRef(true);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
|
||||
const { loading, error } = useOrdersUpdateSubscription({
|
||||
variables: {
|
||||
partyId: partyId || '',
|
||||
},
|
||||
skip: !partyId,
|
||||
fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
|
||||
onData: ({ data }) => {
|
||||
const update = data.data?.orders || [];
|
||||
|
||||
if (loadRef.current) {
|
||||
setOrders(update);
|
||||
loadRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!update.length) return;
|
||||
|
||||
// update orders
|
||||
setOrders((curr) => {
|
||||
// this might not be required all timestamps seem to be the same now
|
||||
const sortedIncoming = orderBy(
|
||||
update,
|
||||
(o) => {
|
||||
if (o.updatedAt) {
|
||||
return new Date(o.updatedAt).getTime();
|
||||
}
|
||||
return new Date(o.createdAt).getTime();
|
||||
},
|
||||
'desc'
|
||||
);
|
||||
const combined = uniqBy([...sortedIncoming, ...curr], 'id');
|
||||
return combined.filter((o) => isOrderActive(o));
|
||||
});
|
||||
},
|
||||
[create]
|
||||
);
|
||||
});
|
||||
|
||||
const onGridReady = useCallback(
|
||||
({ api }: GridReadyEvent) => {
|
||||
if (filter !== undefined) {
|
||||
api.setFilterModel({
|
||||
status: {
|
||||
value: FilterStatusValue[filter],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
return {
|
||||
orders,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
const rowCount = gridRef.current?.api?.getModel().getRowCount();
|
||||
setHasData((rowCount ?? 0) > 0);
|
||||
bottomPlaceholderOnFilterChanged?.();
|
||||
},
|
||||
[bottomPlaceholderOnFilterChanged]
|
||||
);
|
||||
const isOrderActive = (o: { status: OrderStatus }) => {
|
||||
return [
|
||||
OrderStatus.STATUS_ACTIVE,
|
||||
OrderStatus.STATUS_PARKED,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
].includes(o.status);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
|
||||
}, [data]);
|
||||
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
||||
const loadRef = useRef(true);
|
||||
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
||||
const { markets } = useMarkets();
|
||||
const { assets } = useAssets();
|
||||
// Example using react state
|
||||
//
|
||||
// Pass orders as rowData, because we use getRowId the grid should update 'naturally'
|
||||
const { orders } = useOrders(partyId);
|
||||
|
||||
const cancelAll = useCallback(() => {
|
||||
create({
|
||||
orderCancellation: {},
|
||||
});
|
||||
}, [create]);
|
||||
// Example with gridApi.applyTransaction
|
||||
//
|
||||
// in this example the entire order state is in the grid as we
|
||||
// dont cache in apollo and we discard the data once applied to the grid
|
||||
|
||||
// useOrdersUpdateSubscription({
|
||||
// variables: {
|
||||
// partyId: partyId || '',
|
||||
// },
|
||||
// skip: !gridApi || !partyId,
|
||||
// fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
|
||||
// onData: ({ data }) => {
|
||||
// console.log(gridApi);
|
||||
// if (!gridApi) {
|
||||
// throw new Error('Grid not ready');
|
||||
// }
|
||||
|
||||
// const incoming = data.data?.orders || [];
|
||||
|
||||
// if (loadRef.current) {
|
||||
// loadRef.current = false;
|
||||
// gridApi.applyTransaction({
|
||||
// add: incoming,
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!incoming.length) return;
|
||||
|
||||
// const add: Order[] = [];
|
||||
// const remove: Order[] = [];
|
||||
// const update: Order[] = [];
|
||||
|
||||
// incoming.forEach((o) => {
|
||||
// const exists = gridApi.getRowNode(o.id);
|
||||
// if (exists) {
|
||||
// if (isOrderActive(o)) {
|
||||
// update.push(o);
|
||||
// } else {
|
||||
// remove.push(o);
|
||||
// }
|
||||
// } else {
|
||||
// if (isOrderActive(o)) {
|
||||
// add.push(o);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// gridApi.applyTransaction({
|
||||
// add,
|
||||
// remove,
|
||||
// update,
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
const coldefs = useMemo<ColDef[]>(() => {
|
||||
return [
|
||||
{
|
||||
field: 'marketId',
|
||||
valueGetter: ({ data }) => {
|
||||
const market = markets[data.marketId];
|
||||
return market.tradableInstrument.instrument.code;
|
||||
},
|
||||
sortable: true,
|
||||
filter: true,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
},
|
||||
{
|
||||
field: 'side',
|
||||
},
|
||||
{
|
||||
headerName: 'Created',
|
||||
colId: 'createdAt',
|
||||
sort: 'desc', // default sort by latest
|
||||
valueGetter: ({ data }) => new Date(data.createdAt),
|
||||
valueFormatter: ({ value }) => getDateTimeFormat().format(value),
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
},
|
||||
];
|
||||
}, [markets]);
|
||||
|
||||
if (!Object.keys(markets).length) {
|
||||
return <div>No markets</div>;
|
||||
}
|
||||
|
||||
if (!assets.length) {
|
||||
return <div>No assets</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
<OrderListTable
|
||||
rowData={data as Order[]}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onGridReady={onGridReady}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={onFilterChanged}
|
||||
isReadOnly={isReadOnly}
|
||||
blockLoadDebounceMillis={100}
|
||||
storeKey={storeKey}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
suppressAutoSize
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No orders')}
|
||||
noDataCondition={(data) => !hasData}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isReadOnly && hasAmendableOrder && (
|
||||
<CancelAllOrdersButton onClick={cancelAll} />
|
||||
)}
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) setEditOrder(null);
|
||||
}}
|
||||
order={editOrder}
|
||||
onSubmit={(fields) => {
|
||||
if (!editOrder.market) {
|
||||
return;
|
||||
}
|
||||
const orderAmendment = normalizeOrderAmendment(
|
||||
editOrder,
|
||||
editOrder.market,
|
||||
fields.limitPrice,
|
||||
fields.size
|
||||
);
|
||||
const originalOrder: OrderTxUpdateFieldsFragment = {
|
||||
type: editOrder.type,
|
||||
id: editOrder.id,
|
||||
status: editOrder.status,
|
||||
createdAt: editOrder.createdAt,
|
||||
size: editOrder.size,
|
||||
price: editOrder.price,
|
||||
timeInForce: editOrder.timeInForce,
|
||||
expiresAt: editOrder.expiresAt,
|
||||
side: editOrder.side,
|
||||
marketId: editOrder.market.id,
|
||||
};
|
||||
create({ orderAmendment }, originalOrder);
|
||||
setEditOrder(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<AgGridLazy
|
||||
rowData={orders}
|
||||
onGridReady={(event) => {
|
||||
setGridApi(event.api);
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
columnDefs={coldefs}
|
||||
getRowId={(params) => {
|
||||
return params.data.id;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user