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', () => {
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-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
@ -19,7 +34,22 @@ describe('market bottom panel', { tags: '@smoke' }, () => {
cy.viewport(1801, 1000);
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-accounts').should(
'have.attr',

View File

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

View File

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

View File

@ -35,7 +35,7 @@ import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { ENV } from '../lib/config';
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 {
ProtocolUpgradeCountdownMode,
@ -150,6 +150,11 @@ const PartyData = () => {
variables,
skip,
});
useDataProvider({
dataProvider: allOrdersProvider,
variables,
skip,
});
return null;
};

View File

@ -37,9 +37,9 @@ export const AccountManager = ({
variables,
});
const setId = useCallback(
(data: AccountFields) => ({
(data: AccountFields, id: string) => ({
...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;
}, [pinnedAssetId, props.pinnedAsset, props.rowData]);
const getRowHeight = useCallback(
(params: RowHeightParams) =>
const { getRowHeight } = props;
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAssetId &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
? 32
: 24,
[pinnedAssetId]
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
},
[pinnedAssetId, getRowHeight]
);
return (
@ -155,7 +161,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
sortable: true,
comparator: accountValuesComparator,
}}
getRowHeight={getRowHeight}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedAssetRow ? [pinnedAssetRow] : undefined}
>
<AgGridColumn

View File

@ -9,10 +9,11 @@ import {
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
export const SetFilter = forwardRef(
(props: IFilterParams & { readonly?: boolean }, ref) => {
const [value, setValue] = useState<string[]>([]);
const valueRef = useRef(value);
const { readonly } = props;
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
@ -62,7 +63,6 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
useEffect(() => {
props.filterChangedCallback();
}, [value]); //eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="ag-filter-body-wrapper">
<fieldset className="ag-simple-filter-body-wrapper">
@ -71,6 +71,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
<input
type="checkbox"
value={key}
disabled={readonly}
className="mr-1"
checked={value.includes(key)}
onChange={onChange}
@ -79,15 +80,19 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
</label>
))}
</fieldset>
{!readonly && (
<div className="ag-filter-apply-panel">
<button
type="button"
disabled={readonly}
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue((valueRef.current = []))}
>
{t('Reset')}
</button>
</div>
)}
</div>
);
});
}
);

View File

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

View File

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

View File

@ -5,8 +5,6 @@ import {
makeDataProvider,
makeDerivedDataProvider,
defaultAppend as append,
paginatedCombineDelta as combineDelta,
paginatedCombineInsertionData as combineInsertionData,
} from '@vegaprotocol/utils';
import type { Market } 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>;
const liveOnlyOrderStatuses = [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARKED,
];
const orderMatchFilters = (
order: OrderUpdateFieldsFragment,
variables: OrdersQueryVariables
@ -41,6 +44,12 @@ const orderMatchFilters = (
) {
return false;
}
if (
variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status))
) {
return false;
}
if (
variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type))
@ -58,19 +67,13 @@ const orderMatchFilters = (
}
if (
variables?.filter?.dateRange?.start &&
!(
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.start < (order.updatedAt || order.createdAt)
)
!(order.createdAt && variables.filter.dateRange.start < order.createdAt)
) {
return false;
}
if (
variables?.filter?.dateRange?.end &&
!(
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.end > (order.updatedAt || order.createdAt)
)
!(order.createdAt && variables.filter.dateRange.end > order.createdAt)
) {
return false;
}
@ -120,28 +123,25 @@ export const update = (
if (!data) {
return data;
}
return produce(data, (draft) => {
// 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
const incoming = uniqBy(
const incoming = orderBy(
uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
return produce(data, (draft) => {
// Add or update incoming orders
incoming.reverse().forEach((node) => {
incoming.forEach((node) => {
const index = draft.findIndex((edge) => edge.node.id === node.id);
const newer =
draft.length === 0 ||
(node.updatedAt || node.createdAt) >=
(draft[0].node.updatedAt || draft[0].node.createdAt);
draft.length === 0 || node.createdAt >= draft[0].node.createdAt;
const doesFilterPass = !variables || orderMatchFilters(node, variables);
if (index !== -1) {
if (doesFilterPass) {
Object.assign(draft[index].node, node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else {
draft.splice(index, 1);
}
@ -194,6 +194,34 @@ const ordersProvider = makeDataProvider<
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<
ReturnType<typeof getData>,
never,
@ -204,11 +232,12 @@ export const activeOrdersProvider = makeDerivedDataProvider<
ordersProvider(callback, client, {
partyId: variables.partyId,
filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
liveOnly: true,
},
}),
],
(partsData, variables, prevData, parts, subscriptions) => {
// load all pages
if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) {
subscriptions[0].load();
}
@ -220,7 +249,7 @@ export const activeOrdersProvider = makeDerivedDataProvider<
);
export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[],
(Order | null)[],
Order[],
OrdersQueryVariables
>(
@ -228,18 +257,13 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
ordersProvider,
(callback, client) => marketsProvider(callback, client, undefined),
],
(partsData): OrderEdge[] =>
(partsData): Order[] =>
((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({
cursor: edge.cursor,
node: {
...edge.node,
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<

View File

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

View File

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

View File

@ -1,25 +1,41 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { FilterChangedEvent, SortChangedEvent } from 'ag-grid-community';
import { Button } from '@vegaprotocol/ui-toolkit';
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 { useOrderListData } from './use-order-list-data';
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 { OrderStatus } from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import isEqual from 'lodash/isEqual';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
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 {
partyId: string;
@ -28,6 +44,7 @@ export interface OrderListManagerProps {
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
enforceBottomPlaceholder?: boolean;
filter?: Filter;
}
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
@ -43,12 +60,6 @@ const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
</div>
);
const initialFilter: Filter = {
status: {
value: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
};
export const OrderListManager = ({
partyId,
marketId,
@ -56,27 +67,22 @@ export const OrderListManager = ({
onOrderTypeClick,
isReadOnly,
enforceBottomPlaceholder,
filter,
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const scrolledToTop = useRef(false);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>(initialFilter);
const filterRef = useRef(initialFilter);
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 } = useOrderListData({
partyId,
sort,
filter,
gridRef,
scrolledToTop,
const { data, error, loading, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables:
filter === Filter.Open
? { partyId, filter: { liveOnly: true } }
: { partyId },
});
const {
onSortChanged: bottomPlaceholderOnSortChanged,
onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({
@ -84,42 +90,6 @@ export const OrderListManager = ({
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(
(order: Order) => {
if (!order.market) return;
@ -133,12 +103,30 @@ export const OrderListManager = ({
[create]
);
const onGridReady = useCallback(({ api }: GridReadyEvent) => {
api.setFilterModel(initialFilter);
}, []);
const onGridReady = useCallback(
({ 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(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
}, [data]);
const cancelAll = useCallback(() => {
@ -148,26 +136,20 @@ export const OrderListManager = ({
},
});
}, [create, marketId]);
const extractedData =
data && !loading
? data
.filter((item) => item !== null)
.map((item) => (item as OrderEdge).node)
: null;
return (
<>
<div className="h-full relative">
<OrderListTable
rowData={extractedData}
rowData={data as Order[]}
ref={gridRef}
readonlyStatusFilter={filter !== undefined}
onGridReady={onGridReady}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={onFilterChanged}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
@ -180,7 +162,7 @@ export const OrderListManager = ({
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !dataCount}
noDataCondition={(data) => !hasData}
reload={reload}
/>
</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;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
readonlyStatusFilter?: boolean;
isReadOnly: boolean;
};
export const OrderListTable = memo(
forwardRef<AgGridReact, OrderListTableProps>(
(
{ cancel, setEditOrder, onMarketClick, onOrderTypeClick, ...props },
{
cancel,
setEditOrder,
onMarketClick,
onOrderTypeClick,
readonlyStatusFilter,
...props
},
ref
) => {
return (
@ -119,6 +127,7 @@ export const OrderListTable = memo(
filter={SetFilter}
filterParams={{
set: Schema.OrderStatusMapping,
readonly: readonlyStatusFilter,
}}
valueFormatter={({
value,
@ -229,6 +238,7 @@ export const OrderListTable = memo(
/>
<AgGridColumn
field="createdAt"
filter={DateRangeFilter}
cellRenderer={({
data,
value,
@ -243,7 +253,6 @@ export const OrderListTable = memo(
/>
<AgGridColumn
field="updatedAt"
filter={DateRangeFilter}
cellRenderer={({
data,
value,

View File

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

View File

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