feat(trading): divide order tab into open, closed, rejected and all (#3541)

This commit is contained in:
Bartłomiej Głownia 2023-05-04 12:09:55 +02:00 committed by GitHub
parent 768b3b29f0
commit c37f9ebe66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 485 additions and 650 deletions

View File

@ -9,6 +9,21 @@ describe('market bottom panel', { tags: '@smoke' }, () => {
it('on xxl screen should be splitted out into two tables', () => { it('on xxl screen should be splitted out into two tables', () => {
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active'); cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive'); cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive'); cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should( cy.getByTestId('tab-accounts').should(
@ -19,7 +34,22 @@ describe('market bottom panel', { tags: '@smoke' }, () => {
cy.viewport(1801, 1000); cy.viewport(1801, 1000);
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active'); cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'active'); cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive'); cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should( cy.getByTestId('tab-accounts').should(
'have.attr', 'have.attr',

View File

@ -29,7 +29,7 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
cy.mockSubscription(subscriptionMocks); cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet(); cy.setVegaWallet();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.getByTestId('Orders').click(); cy.getByTestId('All').click();
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@ -87,7 +87,7 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
cy.get('[col-id="status"] .ag-icon-menu').click(); cy.get('[col-id="status"] .ag-icon-menu').click();
}); });
cy.contains('Partially Filled').click(); cy.contains('Partially Filled').click();
cy.getByTestId('Orders').click(); cy.getByTestId('All').click();
cy.get(`[row-id="${partiallyFilledId}"]`) cy.get(`[row-id="${partiallyFilledId}"]`)
.eq(1) .eq(1)
@ -116,7 +116,7 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
cy.get('[col-id="status"] .ag-icon-menu').click(); cy.get('[col-id="status"] .ag-icon-menu').click();
}); });
cy.contains('Reset').click(); cy.contains('Reset').click();
cy.getByTestId('Orders').click(); cy.getByTestId('All').click();
cy.getByTestId('tab-orders') cy.getByTestId('tab-orders')
.get(`.ag-center-cols-container [col-id='${orderSymbol}']`) .get(`.ag-center-cols-container [col-id='${orderSymbol}']`)
@ -139,14 +139,15 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
}); });
describe('subscribe orders', { tags: '@smoke' }, () => { describe('subscribe orders', { tags: '@smoke' }, () => {
before(() => { let orderId = '0';
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks(); const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate'); cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks); cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet(); cy.setVegaWallet();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.getByTestId('Orders').click(); cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => { cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]') cy.get('[col-id="status"][role="columnheader"]')
.focus() .focus()
@ -154,8 +155,8 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
.click(); .click();
cy.get('.ag-filter-apply-panel-button').click(); cy.get('.ag-filter-apply-panel-button').click();
}); });
orderId = (parseInt(orderId, 10) + 1).toString();
}); });
const orderId = '1234567890';
// 7002-SORD-053 // 7002-SORD-053
// 7002-SORD-040 // 7002-SORD-040
// 7003-MORD-001 // 7003-MORD-001
@ -299,7 +300,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
}); });
cy.get(`[row-id=${orderId}]`) cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]') .find('[col-id="price"]')
.should('have.text', '200.00'); .should('have.text', '-');
}); });
it('must see the time in force applied to the order', () => { it('must see the time in force applied to the order', () => {
@ -370,7 +371,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
cy.mockSubscription(subscriptionMocks); cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet(); cy.setVegaWallet();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.getByTestId('Orders').click(); cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => { cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]') cy.get('[col-id="status"][role="columnheader"]')
.focus() .focus()

View File

@ -1,7 +1,8 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket'; import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info'; import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth'; import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer } from '@vegaprotocol/orders'; import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills'; import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions'; import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades'; import { TradesContainer } from '@vegaprotocol/trades';
@ -58,17 +59,59 @@ const requiresMarket = (View: MarketDependantView) => {
}; };
const TradingViews = { const TradingViews = {
Candles: requiresMarket(CandlesChartContainer), candles: {
Depth: requiresMarket(DepthChartContainer), label: 'Candles',
Liquidity: requiresMarket(LiquidityContainer), component: requiresMarket(CandlesChartContainer),
Ticket: requiresMarket(DealTicketContainer), },
Info: requiresMarket(MarketInfoAccordionContainer), depth: {
Orderbook: requiresMarket(OrderbookContainer), label: 'Depth',
Trades: requiresMarket(TradesContainer), component: requiresMarket(DepthChartContainer),
Positions: PositionsContainer, },
Orders: OrderListContainer, liquidity: {
Collateral: AccountsContainer, label: 'Liquidity',
Fills: FillsContainer, component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
}; };
type TradingView = keyof typeof TradingViews; type TradingView = keyof typeof TradingViews;
@ -104,9 +147,42 @@ const MarketBottomPanel = memo(
> >
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom-left"> <Tabs storageKey="console-trade-grid-bottom-left">
<Tab id="orders" name={t('Orders')}> <Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Orders <TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId} marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick} onOrderTypeClick={onOrderTypeClick}
@ -116,7 +192,7 @@ const MarketBottomPanel = memo(
</Tab> </Tab>
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Fills <TradingViews.fills.component
marketId={marketId} marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
/> />
@ -134,7 +210,7 @@ const MarketBottomPanel = memo(
<Tabs storageKey="console-trade-grid-bottom-right"> <Tabs storageKey="console-trade-grid-bottom-right">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Positions <TradingViews.positions.component
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
noBottomPlaceholder noBottomPlaceholder
/> />
@ -142,7 +218,7 @@ const MarketBottomPanel = memo(
</Tab> </Tab>
<Tab id="accounts" name={t('Collateral')}> <Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Collateral <TradingViews.collateral.component
pinnedAsset={pinnedAsset} pinnedAsset={pinnedAsset}
noBottomPlaceholder noBottomPlaceholder
hideButtons hideButtons
@ -158,12 +234,45 @@ const MarketBottomPanel = memo(
<Tabs storageKey="console-trade-grid-bottom"> <Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} /> <TradingViews.positions.component onMarketClick={onMarketClick} />
</VegaWalletContainer> </VegaWalletContainer>
</Tab> </Tab>
<Tab id="orders" name={t('Orders')}> <Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Orders <TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId} marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick} onOrderTypeClick={onOrderTypeClick}
@ -173,7 +282,7 @@ const MarketBottomPanel = memo(
</Tab> </Tab>
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Fills <TradingViews.fills.component
marketId={marketId} marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
/> />
@ -181,7 +290,10 @@ const MarketBottomPanel = memo(
</Tab> </Tab>
<Tab id="accounts" name={t('Collateral')}> <Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Collateral pinnedAsset={pinnedAsset} hideButtons /> <TradingViews.collateral.component
pinnedAsset={pinnedAsset}
hideButtons
/>
</VegaWalletContainer> </VegaWalletContainer>
</Tab> </Tab>
</Tabs> </Tabs>
@ -221,13 +333,13 @@ const MainGrid = memo(
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-main-left"> <Tabs storageKey="console-trade-grid-main-left">
<Tab id="chart" name={t('Chart')}> <Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} /> <TradingViews.candles.component marketId={marketId} />
</Tab> </Tab>
<Tab id="depth" name={t('Depth')}> <Tab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={marketId} /> <TradingViews.depth.component marketId={marketId} />
</Tab> </Tab>
<Tab id="liquidity" name={t('Liquidity')}> <Tab id="liquidity" name={t('Liquidity')}>
<TradingViews.Liquidity marketId={marketId} /> <TradingViews.liquidity.component marketId={marketId} />
</Tab> </Tab>
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
@ -240,13 +352,13 @@ const MainGrid = memo(
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-main-center"> <Tabs storageKey="console-trade-grid-main-center">
<Tab id="ticket" name={t('Ticket')}> <Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket <TradingViews.ticket.component
marketId={marketId} marketId={marketId}
onClickCollateral={() => navigate('/portfolio')} onClickCollateral={() => navigate('/portfolio')}
/> />
</Tab> </Tab>
<Tab id="info" name={t('Info')}> <Tab id="info" name={t('Info')}>
<TradingViews.Info marketId={marketId} /> <TradingViews.info.component marketId={marketId} />
</Tab> </Tab>
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
@ -259,10 +371,10 @@ const MainGrid = memo(
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-main-right"> <Tabs storageKey="console-trade-grid-main-right">
<Tab id="orderbook" name={t('Orderbook')}> <Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} /> <TradingViews.orderbook.component marketId={marketId} />
</Tab> </Tab>
<Tab id="trades" name={t('Trades')}> <Tab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={marketId} /> <TradingViews.trades.component marketId={marketId} />
</Tab> </Tab>
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
@ -330,7 +442,7 @@ export const TradePanels = ({
const onMarketClick = useMarketClickHandler(true); const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true); const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('Candles'); const [view, setView] = useState<TradingView>('candles');
const renderView = () => { const renderView = () => {
const Component = memo<{ const Component = memo<{
marketId: string; marketId: string;
@ -339,7 +451,7 @@ export const TradePanels = ({
onOrderTypeClick?: (marketId: string) => void; onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void; onClickCollateral: () => void;
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
}>(TradingViews[view]); }>(TradingViews[view].component);
if (!Component) { if (!Component) {
throw new Error(`No component for view: ${view}`); throw new Error(`No component for view: ${view}`);
@ -388,7 +500,7 @@ export const TradePanels = ({
className={className} className={className}
key={key} key={key}
> >
{key} {TradingViews[key as keyof typeof TradingViews].label}
</button> </button>
); );
})} })}

View File

@ -35,7 +35,7 @@ import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
import { ENV } from '../lib/config'; import { ENV } from '../lib/config';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders'; import { activeOrdersProvider, allOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval'; import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import { import {
ProtocolUpgradeCountdownMode, ProtocolUpgradeCountdownMode,
@ -150,6 +150,11 @@ const PartyData = () => {
variables, variables,
skip, skip,
}); });
useDataProvider({
dataProvider: allOrdersProvider,
variables,
skip,
});
return null; return null;
}; };

View File

@ -37,9 +37,9 @@ export const AccountManager = ({
variables, variables,
}); });
const setId = useCallback( const setId = useCallback(
(data: AccountFields) => ({ (data: AccountFields, id: string) => ({
...data, ...data,
asset: { ...data.asset, id: `${data.asset.id}-1` }, asset: { ...data.asset, id },
}), }),
[] []
); );

View File

@ -126,14 +126,20 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
return currentPinnedAssetRow; return currentPinnedAssetRow;
}, [pinnedAssetId, props.pinnedAsset, props.rowData]); }, [pinnedAssetId, props.pinnedAsset, props.rowData]);
const getRowHeight = useCallback( const { getRowHeight } = props;
(params: RowHeightParams) =>
params.node.rowPinned && const getPinnedAssetRowHeight = useCallback(
params.data.asset.id === pinnedAssetId && (params: RowHeightParams) => {
new BigNumber(params.data.total).isLessThanOrEqualTo(0) if (
? 32 params.node.rowPinned &&
: 24, params.data.asset.id === pinnedAssetId &&
[pinnedAssetId] new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
},
[pinnedAssetId, getRowHeight]
); );
return ( return (
@ -155,7 +161,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
sortable: true, sortable: true,
comparator: accountValuesComparator, comparator: accountValuesComparator,
}} }}
getRowHeight={getRowHeight} getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedAssetRow ? [pinnedAssetRow] : undefined} pinnedTopRowData={pinnedAssetRow ? [pinnedAssetRow] : undefined}
> >
<AgGridColumn <AgGridColumn

View File

@ -9,85 +9,90 @@ import {
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community'; import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const SetFilter = forwardRef((props: IFilterParams, ref) => { export const SetFilter = forwardRef(
const [value, setValue] = useState<string[]>([]); (props: IFilterParams & { readonly?: boolean }, ref) => {
const valueRef = useRef(value); const [value, setValue] = useState<string[]>([]);
const valueRef = useRef(value);
const { readonly } = props;
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
doesFilterPass(params: IDoesFilterPassParams) {
const { api, colDef, column, columnApi, context } = props;
const { node } = params;
const getValue = props.valueGetter({
api,
colDef,
column,
columnApi,
context,
data: node.data,
getValue: (field) => node.data[field],
node,
});
return Array.isArray(value)
? value.includes(getValue)
: getValue === value;
},
// expose AG Grid Filter Lifecycle callbacks isFilterActive() {
useImperativeHandle(ref, () => { return valueRef.current.length !== 0;
return { },
doesFilterPass(params: IDoesFilterPassParams) {
const { api, colDef, column, columnApi, context } = props;
const { node } = params;
const getValue = props.valueGetter({
api,
colDef,
column,
columnApi,
context,
data: node.data,
getValue: (field) => node.data[field],
node,
});
return Array.isArray(value)
? value.includes(getValue)
: getValue === value;
},
isFilterActive() { getModel() {
return valueRef.current.length !== 0; if (!this.isFilterActive()) {
}, return null;
}
return { value: valueRef.current };
},
getModel() { setModel(model?: { value: string[] } | null) {
if (!this.isFilterActive()) { valueRef.current = !model ? [] : model.value;
return null; setValue(valueRef.current);
} },
return { value: valueRef.current }; };
}, });
setModel(model?: { value: string[] } | null) { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
valueRef.current = !model ? [] : model.value; valueRef.current = event.target.checked
setValue(valueRef.current); ? [...value, event.target.value]
}, : value.filter((v) => v !== event.target.value);
setValue(valueRef.current);
}; };
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => { useEffect(() => {
valueRef.current = event.target.checked props.filterChangedCallback();
? [...value, event.target.value] }, [value]); //eslint-disable-line react-hooks/exhaustive-deps
: value.filter((v) => v !== event.target.value); return (
setValue(valueRef.current); <div className="ag-filter-body-wrapper">
}; <fieldset className="ag-simple-filter-body-wrapper">
{Object.keys(props.colDef.filterParams.set).map((key) => (
useEffect(() => { <label className="flex" key={key}>
props.filterChangedCallback(); <input
}, [value]); //eslint-disable-line react-hooks/exhaustive-deps type="checkbox"
value={key}
return ( disabled={readonly}
<div className="ag-filter-body-wrapper"> className="mr-1"
<fieldset className="ag-simple-filter-body-wrapper"> checked={value.includes(key)}
{Object.keys(props.colDef.filterParams.set).map((key) => ( onChange={onChange}
<label className="flex" key={key}> />
<input <span>{props.colDef.filterParams.set[key]}</span>
type="checkbox" </label>
value={key} ))}
className="mr-1" </fieldset>
checked={value.includes(key)} {!readonly && (
onChange={onChange} <div className="ag-filter-apply-panel">
/> <button
<span>{props.colDef.filterParams.set[key]}</span> type="button"
</label> disabled={readonly}
))} className="ag-standard-button ag-filter-apply-panel-button"
</fieldset> onClick={() => setValue((valueRef.current = []))}
<div className="ag-filter-apply-panel"> >
<button {t('Reset')}
type="button" </button>
className="ag-standard-button ag-filter-apply-panel-button" </div>
onClick={() => setValue((valueRef.current = []))} )}
>
{t('Reset')}
</button>
</div> </div>
</div> );
); }
}); );

View File

@ -62,7 +62,7 @@ export const FillsManager = ({
scrolledToTop.current = event.top <= 0; scrolledToTop.current = event.top <= 0;
}, []); }, []);
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules } = const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } =
useBottomPlaceholder<Trade>({ useBottomPlaceholder<Trade>({
gridRef, gridRef,
}); });
@ -82,6 +82,7 @@ export const FillsManager = ({
isFullWidthRow={isFullWidthRow} isFullWidthRow={isFullWidthRow}
fullWidthCellRenderer={fullWidthCellRenderer} fullWidthCellRenderer={fullWidthCellRenderer}
rowClassRules={rowClassRules} rowClassRules={rowClassRules}
getRowHeight={getRowHeight}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<AsyncRenderer <AsyncRenderer

View File

@ -7,15 +7,14 @@ describe('order data provider', () => {
const data = [ const data = [
{ {
node: { node: {
id: '1', id: '2',
updatedAt: new Date('2022-01-31').toISOString(),
createdAt: new Date('2022-01-29').toISOString(), createdAt: new Date('2022-01-29').toISOString(),
}, },
}, },
{ {
node: { node: {
id: '2', id: '1',
createdAt: new Date('2022-01-30').toISOString(), createdAt: new Date('2022-01-28').toISOString(),
}, },
}, },
] as Edge<OrderFieldsFragment>[]; ] as Edge<OrderFieldsFragment>[];
@ -24,47 +23,51 @@ describe('order data provider', () => {
// this one should be dropped because id don't exits and it's older than newest // this one should be dropped because id don't exits and it's older than newest
{ {
id: '0', id: '0',
createdAt: new Date('2022-01-30').toISOString(), createdAt: new Date('2022-01-27').toISOString(),
}, },
// this one should be dropped because newer below // this one should be dropped because newer below
{ {
id: '1', id: '1',
updatedAt: new Date('2022-02-01').toISOString(), updatedAt: new Date('2022-02-01').toISOString(),
createdAt: new Date('2022-01-29').toISOString(), createdAt: new Date('2022-01-28').toISOString(),
}, },
{ {
id: '1', id: '1',
updatedAt: new Date('2022-02-02').toISOString(), updatedAt: new Date('2022-02-04').toISOString(),
createdAt: new Date('2022-01-29').toISOString(), createdAt: new Date('2022-01-28').toISOString(),
}, },
// this should be added // this should be added
{ {
id: '4', id: '4',
createdAt: new Date('2022-02-04').toISOString(), createdAt: new Date('2022-02-04').toISOString(),
}, },
// this should be move to top
{ {
id: '2', id: '2',
updatedAt: new Date('2022-02-03').toISOString(), updatedAt: new Date('2022-02-04').toISOString(),
createdAt: new Date('2022-01-29').toISOString(), createdAt: new Date('2022-01-30').toISOString(),
},
// this should be added
{
id: '5',
createdAt: new Date('2022-02-05').toISOString(),
}, },
] as OrderUpdateFieldsFragment[]; ] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, { partyId: '0x123' }); const updatedData = update(data, delta, () => null, { partyId: '0x123' });
expect( expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id) updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1); ).toEqual(-1);
expect(updatedData && updatedData[2].node.id).toEqual(delta[2].id); expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual( expect(updatedData && updatedData[3].node.updatedAt).toEqual(
delta[2].updatedAt delta[2].updatedAt
); );
expect(updatedData && updatedData[0].node.id).toEqual(delta[3].id); expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].node.id).toEqual(delta[4].id); expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[1].node.updatedAt).toEqual( expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[4].updatedAt delta[4].updatedAt
); );
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual( expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
4 5
); );
}); });
it('add only data matching date range filter', () => { it('add only data matching date range filter', () => {
@ -72,7 +75,6 @@ describe('order data provider', () => {
{ {
node: { node: {
id: '1', id: '1',
updatedAt: new Date('2022-01-31').toISOString(),
createdAt: new Date('2022-01-29').toISOString(), createdAt: new Date('2022-01-29').toISOString(),
}, },
}, },
@ -90,12 +92,6 @@ describe('order data provider', () => {
id: '0', id: '0',
createdAt: new Date('2022-02-02').toISOString(), createdAt: new Date('2022-02-02').toISOString(),
}, },
// this one should be removed because it does not match date range
{
id: '1',
updatedAt: new Date('2022-02-02').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
// this one should be updated // this one should be updated
{ {
id: '2', id: '2',
@ -118,16 +114,13 @@ describe('order data provider', () => {
expect( expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id) updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1); ).toEqual(-1);
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[1].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id); expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual( expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt delta[2].updatedAt
); );
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id); expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id);
expect(updatedData && updatedData[1].node.updatedAt).toEqual( expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[3].updatedAt delta[1].updatedAt
); );
}); });
}); });

View File

@ -5,8 +5,6 @@ import {
makeDataProvider, makeDataProvider,
makeDerivedDataProvider, makeDerivedDataProvider,
defaultAppend as append, defaultAppend as append,
paginatedCombineDelta as combineDelta,
paginatedCombineInsertionData as combineInsertionData,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
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';
@ -28,6 +26,11 @@ export type Order = Omit<OrderFieldsFragment, 'market'> & {
}; };
export type OrderEdge = Edge<Order>; export type OrderEdge = Edge<Order>;
const liveOnlyOrderStatuses = [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARKED,
];
const orderMatchFilters = ( const orderMatchFilters = (
order: OrderUpdateFieldsFragment, order: OrderUpdateFieldsFragment,
variables: OrdersQueryVariables variables: OrdersQueryVariables
@ -41,6 +44,12 @@ const orderMatchFilters = (
) { ) {
return false; return false;
} }
if (
variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status))
) {
return false;
}
if ( if (
variables?.filter?.types && variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type)) !(order.type && variables.filter.types.includes(order.type))
@ -58,19 +67,13 @@ const orderMatchFilters = (
} }
if ( if (
variables?.filter?.dateRange?.start && variables?.filter?.dateRange?.start &&
!( !(order.createdAt && variables.filter.dateRange.start < order.createdAt)
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.start < (order.updatedAt || order.createdAt)
)
) { ) {
return false; return false;
} }
if ( if (
variables?.filter?.dateRange?.end && variables?.filter?.dateRange?.end &&
!( !(order.createdAt && variables.filter.dateRange.end > order.createdAt)
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.end > (order.updatedAt || order.createdAt)
)
) { ) {
return false; return false;
} }
@ -120,28 +123,25 @@ export const update = (
if (!data) { if (!data) {
return data; return data;
} }
return produce(data, (draft) => { // A single update can contain the same order with multiple updates, so we need to find
// A single update can contain the same order with multiple updates, so we need to find // the latest version of the order and only update using that
// the latest version of the order and only update using that const incoming = orderBy(
const incoming = uniqBy( uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'), orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id' 'id'
); ),
'createdAt'
);
return produce(data, (draft) => {
// Add or update incoming orders // Add or update incoming orders
incoming.reverse().forEach((node) => { incoming.forEach((node) => {
const index = draft.findIndex((edge) => edge.node.id === node.id); const index = draft.findIndex((edge) => edge.node.id === node.id);
const newer = const newer =
draft.length === 0 || draft.length === 0 || node.createdAt >= draft[0].node.createdAt;
(node.updatedAt || node.createdAt) >=
(draft[0].node.updatedAt || draft[0].node.createdAt);
const doesFilterPass = !variables || orderMatchFilters(node, variables); const doesFilterPass = !variables || orderMatchFilters(node, variables);
if (index !== -1) { if (index !== -1) {
if (doesFilterPass) { if (doesFilterPass) {
Object.assign(draft[index].node, node); Object.assign(draft[index].node, node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else { } else {
draft.splice(index, 1); draft.splice(index, 1);
} }
@ -194,6 +194,34 @@ const ordersProvider = makeDataProvider<
additionalContext: { isEnlargedTimeout: true }, additionalContext: { isEnlargedTimeout: true },
}); });
const allOrderMaxCount = 50000;
export const allOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
ordersProvider(callback, client, { partyId: variables.partyId }),
],
(partsData, variables, prevData, parts, subscriptions) => {
const orders = partsData[0] as ReturnType<typeof getData>;
// load next pages until allOrderMaxCount reached
if (
!parts[0].isUpdate &&
subscriptions &&
subscriptions[0].load &&
orders?.length < allOrderMaxCount
) {
subscriptions[0].load();
}
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders;
}
);
export const activeOrdersProvider = makeDerivedDataProvider< export const activeOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>, ReturnType<typeof getData>,
never, never,
@ -204,11 +232,12 @@ export const activeOrdersProvider = makeDerivedDataProvider<
ordersProvider(callback, client, { ordersProvider(callback, client, {
partyId: variables.partyId, partyId: variables.partyId,
filter: { filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED], liveOnly: true,
}, },
}), }),
], ],
(partsData, variables, prevData, parts, subscriptions) => { (partsData, variables, prevData, parts, subscriptions) => {
// load all pages
if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) { if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) {
subscriptions[0].load(); subscriptions[0].load();
} }
@ -220,7 +249,7 @@ export const activeOrdersProvider = makeDerivedDataProvider<
); );
export const ordersWithMarketProvider = makeDerivedDataProvider< export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[], (Order | null)[],
Order[], Order[],
OrdersQueryVariables OrdersQueryVariables
>( >(
@ -228,18 +257,13 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
ordersProvider, ordersProvider,
(callback, client) => marketsProvider(callback, client, undefined), (callback, client) => marketsProvider(callback, client, undefined),
], ],
(partsData): OrderEdge[] => (partsData): Order[] =>
((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({ ((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({
cursor: edge.cursor, ...edge.node,
node: { market: (partsData[1] as Market[]).find(
...edge.node, (market) => market.id === edge.node.market.id
market: (partsData[1] as Market[]).find( ),
(market) => market.id === edge.node.market.id }))
),
},
})),
combineDelta<Order, ReturnType<typeof getDelta>['0']>,
combineInsertionData<Order>
); );
export const hasActiveOrderProvider = makeDerivedDataProvider< export const hasActiveOrderProvider = makeDerivedDataProvider<

View File

@ -2,18 +2,23 @@ import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { OrderListManager } from './order-list-manager'; import { OrderListManager } from './order-list-manager';
import type { Filter } from './order-list-manager';
export interface OrderListContainerProps {
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
enforceBottomPlaceholder?: boolean;
filter?: Filter;
}
export const OrderListContainer = ({ export const OrderListContainer = ({
marketId, marketId,
onMarketClick, onMarketClick,
onOrderTypeClick, onOrderTypeClick,
enforceBottomPlaceholder, enforceBottomPlaceholder,
}: { filter,
marketId?: string; }: OrderListContainerProps) => {
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
enforceBottomPlaceholder?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
if (!pubKey) { if (!pubKey) {
@ -24,6 +29,7 @@ export const OrderListContainer = ({
<OrderListManager <OrderListManager
partyId={pubKey} partyId={pubKey}
marketId={marketId} marketId={marketId}
filter={filter}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick} onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}

View File

@ -1,2 +1 @@
export * from './order-list-manager'; export * from './order-list-manager';
export * from './use-order-list-data';

View File

@ -1,25 +1,41 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import type { FilterChangedEvent, SortChangedEvent } from 'ag-grid-community';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { GridReadyEvent } from 'ag-grid-community'; import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list'; import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order'; import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import type { Filter, Sort } from './use-order-list-data';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers'; import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { OrderStatus } from '@vegaprotocol/types'; import { useDataProvider } from '@vegaprotocol/react-helpers';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import { import {
normalizeOrderAmendment, normalizeOrderAmendment,
useVegaTransactionStore, useVegaTransactionStore,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import isEqual from 'lodash/isEqual';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet'; import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog'; import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order, OrderEdge } from '../order-data-provider'; import type { Order } from '../order-data-provider';
import { OrderStatus } from '@vegaprotocol/types';
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 { export interface OrderListManagerProps {
partyId: string; partyId: string;
@ -28,6 +44,7 @@ export interface OrderListManagerProps {
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void; onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean; isReadOnly: boolean;
enforceBottomPlaceholder?: boolean; enforceBottomPlaceholder?: boolean;
filter?: Filter;
} }
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => ( const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
@ -43,12 +60,6 @@ const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
</div> </div>
); );
const initialFilter: Filter = {
status: {
value: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
};
export const OrderListManager = ({ export const OrderListManager = ({
partyId, partyId,
marketId, marketId,
@ -56,27 +67,22 @@ export const OrderListManager = ({
onOrderTypeClick, onOrderTypeClick,
isReadOnly, isReadOnly,
enforceBottomPlaceholder, enforceBottomPlaceholder,
filter,
}: OrderListManagerProps) => { }: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0); const [hasData, setHasData] = useState(false);
const scrolledToTop = useRef(false);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>(initialFilter);
const filterRef = useRef(initialFilter);
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 hasAmendableOrder = useHasAmendableOrder(marketId); const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useDataProvider({
const { data, error, loading, reload } = useOrderListData({ dataProvider: ordersWithMarketProvider,
partyId, variables:
sort, filter === Filter.Open
filter, ? { partyId, filter: { liveOnly: true } }
gridRef, : { partyId },
scrolledToTop,
}); });
const { const {
onSortChanged: bottomPlaceholderOnSortChanged,
onFilterChanged: bottomPlaceholderOnFilterChanged, onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps ...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({ } = useBottomPlaceholder<Order>({
@ -84,42 +90,6 @@ export const OrderListManager = ({
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder, disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
}); });
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (isEqual(updatedFilter, filterRef.current)) {
return;
}
filterRef.current = updatedFilter;
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else {
setFilter(undefined);
}
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
bottomPlaceholderOnFilterChanged?.();
},
[setFilter, bottomPlaceholderOnFilterChanged]
);
const onSortChange = useCallback(
(event: SortChangedEvent) => {
const sort = event.columnApi
.getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
.reduce((acc, col) => {
if (col.sort) {
const { colId, sort } = col;
acc.push({ colId, sort });
}
return acc;
}, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined);
bottomPlaceholderOnSortChanged?.();
},
[setSort, bottomPlaceholderOnSortChanged]
);
const cancel = useCallback( const cancel = useCallback(
(order: Order) => { (order: Order) => {
if (!order.market) return; if (!order.market) return;
@ -133,12 +103,30 @@ export const OrderListManager = ({
[create] [create]
); );
const onGridReady = useCallback(({ api }: GridReadyEvent) => { const onGridReady = useCallback(
api.setFilterModel(initialFilter); ({ api }: GridReadyEvent) => {
}, []); if (filter !== undefined) {
api.setFilterModel({
status: {
value: FilterStatusValue[filter],
},
});
}
},
[filter]
);
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const rowCount = gridRef.current?.api?.getModel().getRowCount();
setHasData((rowCount ?? 0) > 0);
bottomPlaceholderOnFilterChanged?.();
},
[bottomPlaceholderOnFilterChanged]
);
useEffect(() => { useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
}, [data]); }, [data]);
const cancelAll = useCallback(() => { const cancelAll = useCallback(() => {
@ -148,26 +136,20 @@ export const OrderListManager = ({
}, },
}); });
}, [create, marketId]); }, [create, marketId]);
const extractedData =
data && !loading
? data
.filter((item) => item !== null)
.map((item) => (item as OrderEdge).node)
: null;
return ( return (
<> <>
<div className="h-full relative"> <div className="h-full relative">
<OrderListTable <OrderListTable
rowData={extractedData} rowData={data as Order[]}
ref={gridRef} ref={gridRef}
readonlyStatusFilter={filter !== undefined}
onGridReady={onGridReady} onGridReady={onGridReady}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={cancel} cancel={cancel}
setEditOrder={setEditOrder} setEditOrder={setEditOrder}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick} onOrderTypeClick={onOrderTypeClick}
onFilterChanged={onFilterChanged}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
blockLoadDebounceMillis={100} blockLoadDebounceMillis={100}
suppressLoadingOverlay suppressLoadingOverlay
@ -180,7 +162,7 @@ export const OrderListManager = ({
error={error} error={error}
data={data} data={data}
noDataMessage={t('No orders')} noDataMessage={t('No orders')}
noDataCondition={(data) => !dataCount} noDataCondition={(data) => !hasData}
reload={reload} reload={reload}
/> />
</div> </div>

View File

@ -1,166 +0,0 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook, waitFor } from '@testing-library/react';
import { useOrderListData } from './use-order-list-data';
import type { Edge } from '@vegaprotocol/utils';
import type { OrderFieldsFragment } from '../order-data-provider/__generated__/Orders';
import type { IGetRowsParams } from 'ag-grid-community';
const loadMock = jest.fn();
let mockData: Edge<OrderFieldsFragment>[] | null = null;
let mockDataProviderData = {
data: mockData as (Edge<OrderFieldsFragment> | null)[] | null,
error: undefined,
loading: true,
load: loadMock,
totalCount: undefined,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('useOrderListData Hook', () => {
const mockRefreshAgGridApi = jest.fn();
const partyId = 'partyId';
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshAgGridApi,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
const scrolledToTop = {
current: false,
};
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper dataProvider results', () => {
const { result } = renderHook(
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: null,
error: undefined,
loading: true,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
});
it('return proper mocked results', () => {
mockData = [
{
node: {
id: 'data_id_1',
createdAt: 1,
},
} as unknown as Edge<OrderFieldsFragment>,
{
node: {
id: 'data_id_2',
createdAt: 2,
},
} as unknown as Edge<OrderFieldsFragment>,
];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: mockData,
error: undefined,
loading: false,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
updateMock({ data: mockData, delta: [] });
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});
it('methods for pagination should work', async () => {
const successCallback = jest.fn();
mockData = [
{
node: {
id: 'data_id_1',
createdAt: 1,
},
} as unknown as Edge<OrderFieldsFragment>,
{
node: {
id: 'data_id_2',
createdAt: 2,
},
} as unknown as Edge<OrderFieldsFragment>,
];
Object.assign(mockDataProviderData, {
data: mockData,
loading: false,
});
const mockDelta = [
{
node: {
id: 'data_id_3',
createdAt: 3,
},
} as unknown as Edge<OrderFieldsFragment>,
{
node: {
id: 'data_id_4',
createdAt: 4,
},
} as unknown as Edge<OrderFieldsFragment>,
];
const mockNextData = [...mockData, ...mockDelta];
const { result } = renderHook(
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
const getRowsParams = {
successCallback,
failCallback: jest.fn(),
startRow: 2,
endRow: 4,
} as unknown as IGetRowsParams;
await waitFor(async () => {
updateMock({ data: mockData });
});
await waitFor(async () => {
const promise = result.current.getRows(getRowsParams);
updateMock({ data: mockNextData, delta: mockDelta });
await promise;
});
expect(loadMock).toHaveBeenCalled();
expect(successCallback).toHaveBeenLastCalledWith(
mockDelta.map((item) => item.node),
undefined
);
});
});

View File

@ -1,171 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/utils';
import { useDataProvider, updateGridData } from '@vegaprotocol/react-helpers';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import type {
OrderEdge,
Order,
} from '../order-data-provider/order-data-provider';
import type {
OrdersQueryVariables,
OrdersUpdateSubscriptionVariables,
} from '../order-data-provider/__generated__/Orders';
import type * as Types from '@vegaprotocol/types';
export interface Sort {
colId: string;
sort: string;
}
export interface Filter {
updatedAt?: {
value: Types.DateRange;
};
type?: {
value: Types.OrderType[];
};
status?: {
value: Types.OrderStatus[];
};
timeInForce?: {
value: Types.OrderTimeInForce[];
};
}
interface Props {
partyId: string;
marketId?: string;
filter?: Filter;
sort?: Sort[];
gridRef: RefObject<AgGridReact>;
scrolledToTop: RefObject<boolean>;
}
export const useOrderListData = ({
partyId,
marketId,
sort,
filter,
gridRef,
scrolledToTop,
}: Props) => {
const dataRef = useRef<(OrderEdge | null)[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const placeholderAdded = useRef(-1);
const makeBottomPlaceholders = useCallback((order?: Order) => {
if (!order) {
if (placeholderAdded.current >= 0) {
dataRef.current?.splice(placeholderAdded.current, 1);
}
placeholderAdded.current = -1;
} else if (placeholderAdded.current === -1) {
dataRef.current?.push({
node: { ...order, id: `${order?.id}-1`, isLastPlaceholder: true },
});
placeholderAdded.current = (dataRef.current?.length || 0) - 1;
}
}, []);
const variables = useMemo(() => {
// define variable as const to get type safety, using generic with useMemo resulted in lost type safety
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId,
};
if (
filter?.updatedAt?.value ||
filter?.status?.value.length ||
filter?.timeInForce?.value.length ||
filter?.type?.value.length
) {
allVars.filter = {};
if (filter?.updatedAt?.value) {
allVars.filter.dateRange = filter?.updatedAt?.value;
}
if (filter?.status?.value.length) {
allVars.filter.status = filter?.status?.value;
}
if (filter?.timeInForce?.value.length) {
allVars.filter.timeInForce = filter?.timeInForce?.value;
}
if (filter?.type?.value.length) {
allVars.filter.types = filter?.type?.value;
}
}
return allVars;
}, [partyId, filter]);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
gridRef.current?.api?.refreshInfiniteCache();
}, [gridRef]);
const update = useCallback(
({
data,
delta,
}: {
data: (OrderEdge | null)[] | null;
delta?: Order[];
totalCount?: number;
}) => {
if (dataRef.current?.length && delta?.length && !scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += (delta || []).filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
return updateGridData(dataRef, data, gridRef);
}
return false;
},
[gridRef, scrolledToTop]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: (OrderEdge | null)[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
return updateGridData(dataRef, data, gridRef);
}
return false;
},
[gridRef]
);
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider,
update,
insert,
variables,
});
totalCountRef.current = totalCount;
const getRows = useRef(
makeInfiniteScrollGetRows<OrderEdge>(dataRef, totalCountRef, load, newRows)
);
return {
loading,
error,
data,
addNewRows,
getRows: getRows.current,
reload,
makeBottomPlaceholders,
};
};

View File

@ -33,13 +33,21 @@ export type OrderListTableProps = OrderListProps & {
setEditOrder: (order: Order) => void; setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void; onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
readonlyStatusFilter?: boolean;
isReadOnly: boolean; isReadOnly: boolean;
}; };
export const OrderListTable = memo( export const OrderListTable = memo(
forwardRef<AgGridReact, OrderListTableProps>( forwardRef<AgGridReact, OrderListTableProps>(
( (
{ cancel, setEditOrder, onMarketClick, onOrderTypeClick, ...props }, {
cancel,
setEditOrder,
onMarketClick,
onOrderTypeClick,
readonlyStatusFilter,
...props
},
ref ref
) => { ) => {
return ( return (
@ -119,6 +127,7 @@ export const OrderListTable = memo(
filter={SetFilter} filter={SetFilter}
filterParams={{ filterParams={{
set: Schema.OrderStatusMapping, set: Schema.OrderStatusMapping,
readonly: readonlyStatusFilter,
}} }}
valueFormatter={({ valueFormatter={({
value, value,
@ -229,6 +238,7 @@ export const OrderListTable = memo(
/> />
<AgGridColumn <AgGridColumn
field="createdAt" field="createdAt"
filter={DateRangeFilter}
cellRenderer={({ cellRenderer={({
data, data,
value, value,
@ -243,7 +253,6 @@ export const OrderListTable = memo(
/> />
<AgGridColumn <AgGridColumn
field="updatedAt" field="updatedAt"
filter={DateRangeFilter}
cellRenderer={({ cellRenderer={({
data, data,
value, value,

View File

@ -55,10 +55,10 @@ export const PositionsManager = ({
}, },
}); });
const setId = useCallback((data: Position) => { const setId = useCallback((data: Position, id: string) => {
return { return {
...data, ...data,
marketId: `${data.marketId}-1`, marketId: id,
}; };
}, []); }, []);
const bottomPlaceholderProps = useBottomPlaceholder<Position>({ const bottomPlaceholderProps = useBottomPlaceholder<Position>({

View File

@ -1,16 +1,17 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { IsFullWidthRowParams } from 'ag-grid-community'; import type { IsFullWidthRowParams, RowHeightParams } from 'ag-grid-community';
const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' }; const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' };
const ROW_ID = 'bottomPlaceholder';
const fullWidthCellRenderer = () => null; const fullWidthCellRenderer = () => null;
const isFullWidthRow = (params: IsFullWidthRowParams) => const isFullWidthRow = (params: IsFullWidthRowParams) =>
params.rowNode.data?.isLastPlaceholder; params.rowNode.data?.isLastPlaceholder;
interface Props<T> { interface Props<T> {
gridRef: RefObject<AgGridReact>; gridRef: RefObject<AgGridReact>;
setId?: (data: T) => T; setId?: (data: T, id: string) => T;
disabled?: boolean; disabled?: boolean;
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
@ -20,45 +21,42 @@ export const useBottomPlaceholder = <T extends {}>({
disabled, disabled,
}: Props<T>) => { }: Props<T>) => {
const onBodyScrollEnd = useCallback(() => { const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getModel().getRowCount() ?? 0; const rowCont = gridRef.current?.api.getDisplayedRowCount() ?? 0;
const lastRowIndex = gridRef.current?.api.getLastDisplayedRow() ?? 0; if (rowCont) {
if (lastRowIndex && rowCont - 1 === lastRowIndex) { const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(rowCont - 1);
const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(lastRowIndex); if (lastRow && lastRow.data) {
if (lastRow?.data && !lastRow?.data.isLastPlaceholder) { const placeholderRow = setId
const newData = setId ? setId({ ...lastRow.data, isLastPlaceholder: true }, ROW_ID)
? setId({ ...lastRow.data, isLastPlaceholder: true })
: { : {
...lastRow.data, ...lastRow.data,
isLastPlaceholder: true, isLastPlaceholder: true,
id: `${lastRow.data?.id || '-'}-1`, id: ROW_ID,
}; };
const add = [newData]; const transaction = gridRef.current?.api.getRowNode(ROW_ID)
const newIndex = lastRowIndex + 1; ? { update: [placeholderRow] }
gridRef.current?.api.applyTransaction({ : { add: [placeholderRow] };
add, gridRef.current?.api.applyTransaction(transaction);
addIndex: newIndex,
});
const newLastRow =
gridRef.current?.api.getDisplayedRowAtIndex(newIndex);
newLastRow?.setRowHeight(50);
gridRef.current?.api.onRowHeightChanged();
} }
} }
}, [gridRef, setId]); }, [gridRef, setId]);
const onRowsChanged = useCallback(() => { const onRowsChanged = useCallback(() => {
const remove: T[] = []; const placeholderNode = gridRef.current?.api.getRowNode(ROW_ID);
gridRef.current?.api.forEachNodeAfterFilterAndSort((rowNode) => { if (placeholderNode) {
if (rowNode.data.isLastPlaceholder) { const transaction = {
remove.push(rowNode.data); remove: [placeholderNode.data],
} };
}); gridRef.current?.api.applyTransaction(transaction);
gridRef.current?.api.applyTransaction({ }
remove,
});
onBodyScrollEnd(); onBodyScrollEnd();
}, [gridRef, onBodyScrollEnd]); }, [gridRef, onBodyScrollEnd]);
const getRowHeight = useCallback(
(params: RowHeightParams) =>
params.data?.isLastPlaceholder ? 50 : undefined,
[]
);
return useMemo( return useMemo(
() => () =>
!disabled !disabled
@ -69,8 +67,9 @@ export const useBottomPlaceholder = <T extends {}>({
fullWidthCellRenderer, fullWidthCellRenderer,
onSortChanged: onRowsChanged, onSortChanged: onRowsChanged,
onFilterChanged: onRowsChanged, onFilterChanged: onRowsChanged,
getRowHeight,
} }
: {}, : {},
[onBodyScrollEnd, onRowsChanged, disabled] [onBodyScrollEnd, onRowsChanged, disabled, getRowHeight]
); );
}; };