Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8990c1185c | ||
|
|
375f9b07ea |
@@ -1,42 +1,23 @@
|
|||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import compact from 'lodash/compact';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community';
|
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||||
|
import { useMarketsQuery } from '@vegaprotocol/markets';
|
||||||
import { OrderListTable } from '../order-list/order-list';
|
import { useAssetsQuery } from '@vegaprotocol/assets';
|
||||||
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
|
import { AgGridLazy } from '@vegaprotocol/datagrid';
|
||||||
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
|
import type { OrdersUpdateSubscription } from '../order-data-provider';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useOrdersUpdateSubscription } from '../order-data-provider';
|
||||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
import type { ArrayElement } from 'type-fest/source/internal';
|
||||||
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 { OrderStatus } from '@vegaprotocol/types';
|
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 {
|
export enum Filter {
|
||||||
'Open',
|
'Open',
|
||||||
'Closed',
|
'Closed',
|
||||||
'Rejected',
|
'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 {
|
export interface OrderListManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
marketId?: string;
|
marketId?: string;
|
||||||
@@ -48,164 +29,209 @@ export interface OrderListManagerProps {
|
|||||||
storeKey?: string;
|
storeKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
|
const useMarkets = () => {
|
||||||
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
|
const { data, loading, error } = useMarketsQuery();
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onClick}
|
|
||||||
data-testid="cancelAll"
|
|
||||||
>
|
|
||||||
{t('Cancel all')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const OrderListManager = ({
|
const markets = data?.marketsConnection?.edges.map((e) => e.node) || [];
|
||||||
partyId,
|
const marketLookup: Record<string, MarketFieldsFragment> = {};
|
||||||
marketId,
|
|
||||||
onMarketClick,
|
markets.forEach((m) => {
|
||||||
onOrderTypeClick,
|
marketLookup[m.id] = m;
|
||||||
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 {
|
return {
|
||||||
onFilterChanged: bottomPlaceholderOnFilterChanged,
|
markets: marketLookup,
|
||||||
...bottomPlaceholderProps
|
data,
|
||||||
} = useBottomPlaceholder({
|
loading,
|
||||||
gridRef,
|
error,
|
||||||
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
const cancel = useCallback(
|
const useAssets = () => {
|
||||||
(order: Order) => {
|
const { data, loading, error } = useAssetsQuery();
|
||||||
if (!order.market) return;
|
return {
|
||||||
create({
|
assets: compact(data?.assetsConnection?.edges || []).map((e) => e.node),
|
||||||
orderCancellation: {
|
data,
|
||||||
orderId: order.id,
|
loading,
|
||||||
marketId: order.market.id,
|
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(
|
return {
|
||||||
({ api }: GridReadyEvent) => {
|
orders,
|
||||||
if (filter !== undefined) {
|
loading,
|
||||||
api.setFilterModel({
|
error,
|
||||||
status: {
|
};
|
||||||
value: FilterStatusValue[filter],
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[filter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFilterChanged = useCallback(
|
const isOrderActive = (o: { status: OrderStatus }) => {
|
||||||
(event: FilterChangedEvent) => {
|
return [
|
||||||
const rowCount = gridRef.current?.api?.getModel().getRowCount();
|
OrderStatus.STATUS_ACTIVE,
|
||||||
setHasData((rowCount ?? 0) > 0);
|
OrderStatus.STATUS_PARKED,
|
||||||
bottomPlaceholderOnFilterChanged?.();
|
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||||
},
|
].includes(o.status);
|
||||||
[bottomPlaceholderOnFilterChanged]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
||||||
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
|
const loadRef = useRef(true);
|
||||||
}, [data]);
|
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(() => {
|
// Example with gridApi.applyTransaction
|
||||||
create({
|
//
|
||||||
orderCancellation: {},
|
// 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
|
||||||
}, [create]);
|
|
||||||
|
// 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 (
|
return (
|
||||||
<>
|
<AgGridLazy
|
||||||
<div className="h-full relative">
|
rowData={orders}
|
||||||
<OrderListTable
|
onGridReady={(event) => {
|
||||||
rowData={data as Order[]}
|
setGridApi(event.api);
|
||||||
ref={gridRef}
|
}}
|
||||||
filter={filter}
|
style={{ width: '100%', height: '100%' }}
|
||||||
onGridReady={onGridReady}
|
columnDefs={coldefs}
|
||||||
onCancel={cancel}
|
getRowId={(params) => {
|
||||||
onEdit={setEditOrder}
|
return params.data.id;
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user