chore(trading): 2825 buttons floating over table rows (#3084)

This commit is contained in:
Maciek 2023-03-08 15:58:40 +01:00 committed by GitHub
parent 8f5a2276de
commit 197f2e8097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 714 additions and 592 deletions

View File

@ -2,10 +2,16 @@ import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import {
useDataProvider,
useBottomPlaceholder,
} from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error, reload } = useDataProvider({
dataProvider: depositsProvider,
@ -13,13 +19,15 @@ export const DepositsContainer = () => {
skip: !pubKey,
});
const openDepositDialog = useDepositDialog((state) => state.open);
const bottomPlaceholderProps = useBottomPlaceholder({ gridRef });
return (
<div className="h-full grid grid-rows-[1fr,min-content]">
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data || []}
noRowsOverlayComponent={() => null}
ref={gridRef}
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@ -33,8 +41,9 @@ export const DepositsContainer = () => {
</div>
</div>
{!isReadOnly && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
onClick={() => openDepositDialog()}
data-testid="deposit-button"

View File

@ -20,36 +20,35 @@ export const WithdrawalsContainer = () => {
return (
<VegaWalletContainer>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full relative">
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={data}
noRowsOverlayComponent={() => null}
<div className="h-full relative">
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={data}
noRowsOverlayComponent={() => null}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
data={data}
loading={loading}
error={error}
noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No withdrawals')}
reload={reload}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
data={data}
loading={loading}
error={error}
noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No withdrawals')}
reload={reload}
/>
</div>
</div>
{!isReadOnly && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
)}
</div>
{!isReadOnly && (
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
onClick={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
)}
</VegaWalletContainer>
);
};

View File

@ -8,12 +8,15 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
import { useDepositDialog } from '@vegaprotocol/deposits';
import { useParams } from 'react-router-dom';
export const AccountsContainer = ({
pinnedAsset,
}: {
pinnedAsset?: PinnedAsset;
}) => {
const params = useParams();
const hideButtons = 'marketId' in params;
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
@ -36,27 +39,30 @@ export const AccountsContainer = ({
}
return (
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div>
<AccountManager
partyId={pubKey}
onClickAsset={onClickAsset}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
/>
</div>
{!isReadOnly && (
<div className="flex gap-2 justify-end p-2 px-[11px]">
<div className="h-full relative">
<AccountManager
partyId={pubKey}
onClickAsset={onClickAsset}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
/>
{!isReadOnly && !hideButtons && (
<div className="flex gap-2 justify-end p-2 px-[11px] fixed bottom-0 right-2 dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
data-testid="open-transfer-dialog"
onClick={() => openTransferDialog()}
>
{t('Transfer')}
</Button>
<Button size="sm" onClick={() => openDepositDialog()}>
<Button
variant="primary"
size="sm"
onClick={() => openDepositDialog()}
>
{t('Deposit')}
</Button>
</div>

View File

@ -14,7 +14,7 @@ export const Footer = () => {
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
return (
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300">
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 fixed bottom-0 left-0 border-r bg-white dark:bg-black">
{/* Pull left to align with top nav, due to button padding */}
<div className="-ml-2">
{VEGA_URL && (

View File

@ -72,7 +72,7 @@ function AppBody({ Component }: AppProps) {
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),1fr,min-content]'
'grid-rows-[repeat(3,min-content),1fr]'
);
return (

View File

@ -1,12 +1,16 @@
import { useRef, useMemo, memo, useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import {
useDataProvider,
useBottomPlaceholder,
} from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { useRef, useMemo, memo } from 'react';
import type { AccountFields } from './accounts-data-provider';
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table';
import type { RowHeightParams } from 'ag-grid-community';
interface AccountManagerProps {
partyId: string;
@ -27,7 +31,6 @@ export const AccountManager = ({
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
const { data, loading, error, reload } = useDataProvider<
AccountFields[],
never
@ -35,6 +38,22 @@ export const AccountManager = ({
dataProvider: aggregatedAccountsDataProvider,
variables,
});
const setId = useCallback(
(data: AccountFields) => ({
...data,
asset: { ...data.asset, id: `${data.asset.id}-1` },
}),
[]
);
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
gridRef,
setId,
});
const getRowHeight = useCallback(
(params: RowHeightParams) => (params.node.rowPinned ? 32 : 22),
[]
);
return (
<div className="relative h-full">
<AccountTable
@ -46,6 +65,8 @@ export const AccountManager = ({
isReadOnly={isReadOnly}
noRowsOverlayComponent={() => null}
pinnedAsset={pinnedAsset}
getRowHeight={getRowHeight}
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -5,9 +5,12 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
import { Button, ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
import {
AgGridDynamic as AgGrid,
CenteredGridCellWrapper,
} from '@vegaprotocol/datagrid';
import { AgGridColumn } from 'ag-grid-react';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
@ -86,18 +89,23 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
cellRenderer={({
value,
data,
node,
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
return value ? (
<ButtonLink
data-testid="asset"
onClick={() => {
if (data) {
onClickAsset(data.asset.id);
}
}}
<CenteredGridCellWrapper
className={node.rowPinned ? 'h-[30px]' : undefined}
>
{value}
</ButtonLink>
<ButtonLink
data-testid="asset"
onClick={() => {
if (data) {
onClickAsset(data.asset.id);
}
}}
>
{value}
</ButtonLink>
</CenteredGridCellWrapper>
) : null;
}}
maxWidth={300}
@ -109,16 +117,25 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
headerTooltip={t(
'This is the total amount of collateral used plus the amount available in your general account.'
)}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'deposited'>) =>
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals)
}
maxWidth={300}
cellRenderer={({
data,
value,
node,
}: VegaICellRendererParams<AccountFields, 'deposited'>) => {
const valueFormatted =
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals);
return node.rowPinned ? (
<CenteredGridCellWrapper className="h-[30px] justify-end">
{valueFormatted}
</CenteredGridCellWrapper>
) : (
valueFormatted
);
}}
/>
<AgGridColumn
headerName={t('Used')}
@ -127,16 +144,25 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
headerTooltip={t(
'This is the amount of collateral used from your general account.'
)}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'used'>) =>
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals)
}
maxWidth={300}
cellRenderer={({
data,
value,
node,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
const valueFormatted =
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals);
return node.rowPinned ? (
<CenteredGridCellWrapper className="h-[30px] justify-end">
{valueFormatted}
</CenteredGridCellWrapper>
) : (
valueFormatted
);
}}
/>
<AgGridColumn
headerName={t('Available')}
@ -155,73 +181,93 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
addDecimalsFormatNumber(value, data.asset.decimals)
}
maxWidth={300}
cellRenderer={({
data,
value,
node,
}: VegaICellRendererParams<AccountFields, 'available'>) => {
const valueFormatted =
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals);
return node.rowPinned ? (
<CenteredGridCellWrapper className="h-[30px] justify-end">
{valueFormatted}
</CenteredGridCellWrapper>
) : (
valueFormatted
);
}}
/>
{
<AgGridColumn
colId="breakdown"
headerName=""
sortable={false}
minWidth={200}
type="rightAligned"
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
if (!data) return null;
else {
if (
data.asset.id === pinnedAssetId &&
new BigNumber(data.deposited).isLessThanOrEqualTo(0)
) {
return (
<ButtonLink
<AgGridColumn
colId="breakdown"
headerName=""
sortable={false}
minWidth={200}
type="rightAligned"
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
if (!data) return null;
else {
if (
data.asset.id === pinnedAssetId &&
new BigNumber(data.deposited).isLessThanOrEqualTo(0)
) {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit to trade')}
</ButtonLink>
);
}
return (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(data.breakdown || null);
}}
>
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
{!props.isReadOnly && (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
)}
<span className="mx-1" />
{!props.isReadOnly && (
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
)}
</>
</Button>
</CenteredGridCellWrapper>
);
}
}}
/>
}
return (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(data.breakdown || null);
}}
>
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
{!props.isReadOnly && (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
)}
<span className="mx-1" />
{!props.isReadOnly && (
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
)}
</>
);
}
}}
/>
</AgGrid>
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
<div className="h-[35vh] w-full m-auto flex flex-col">

View File

@ -8,6 +8,7 @@ export * from './lib/cells/price-cell';
export * from './lib/cells/price-change-cell';
export * from './lib/cells/price-flash-cell';
export * from './lib/cells/vol-cell';
export * from './lib/cells/centered-grid-cell';
export * from './lib/filters/date-range-filter';
export * from './lib/filters/set-filter';

View File

@ -22,7 +22,9 @@ const agGridDarkVariables = `
border-width: 1px 0;
border-bottom: 1px solid transparent;
}
.ag-theme-balham-dark .ag-row.no-hover, .ag-theme-balham-dark .ag-row.no-hover:hover {
background: black;
}
.ag-theme-balham-dark .ag-react-container {
overflow: hidden;
text-overflow: ellipsis;

View File

@ -22,7 +22,9 @@ const agGridLightVariables = `
border-width: 1px 0;
border-bottom: 1px solid transparent;
}
.ag-theme-balham .ag-row.no-hover, .ag-theme-balham .ag-row.no-hover:hover {
background: white;
}
.ag-theme-balham .ag-react-container {
overflow: hidden;
text-overflow: ellipsis;

View File

@ -0,0 +1,16 @@
import type { ReactNode } from 'react';
import classNames from 'classnames';
export const CenteredGridCellWrapper = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => (
<div
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
>
<div className="self-center">{children}</div>
</div>
);

View File

@ -72,7 +72,9 @@ export const DepositsTable = forwardRef<
field="txHash"
cellRenderer={({
value,
data,
}: VegaICellRendererParams<DepositFieldsFragment, 'txHash'>) => {
if (!data) return null;
if (!value) return '-';
return (
<Link

View File

@ -23,6 +23,7 @@ import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
export type Order = Omit<OrderFieldsFragment, 'market'> & {
market?: Market;
isLastPlaceholder?: boolean;
};
export type OrderEdge = Edge<Order>;

View File

@ -1,5 +1,4 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useCallback, useRef, useState } from 'react';
import type {
@ -8,28 +7,19 @@ import type {
FilterChangedEvent,
SortChangedEvent,
} from 'ag-grid-community';
import { Button, Intent } from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import type { Filter, Sort } from './use-order-list-data';
import { useEnvironment } from '@vegaprotocol/environment';
import { Link } from '@vegaprotocol/ui-toolkit';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import type {
VegaTxState,
TransactionResult,
OrderTxUpdateFieldsFragment,
} from '@vegaprotocol/wallet';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { OrderSubFieldsFragment } from '../../order-hooks';
import * as Schema from '@vegaprotocol/types';
import type { Order } from '../order-data-provider';
export interface OrderListManagerProps {
@ -39,41 +29,6 @@ export interface OrderListManagerProps {
isReadOnly: boolean;
}
export const TransactionComplete = ({
transaction,
transactionResult,
}: {
transaction: VegaTxState;
transactionResult?: TransactionResult;
}) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
if (!transactionResult) return null;
return (
<>
{transactionResult.status ? (
<p>{t('Transaction successful')}</p>
) : (
<p className="text-vega-pink">
{t('Transaction failed')}: {transactionResult.error}
</p>
)}
{transaction.txHash && (
<>
<p className="font-semibold mt-4">{t('Transaction')}</p>
<p>
<Link
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
target="_blank"
>
{truncateByChars(transaction.txHash)}
</Link>
</p>
</>
)}
</>
);
};
const CancelAllOrdersButton = ({
onClick,
marketId,
@ -83,8 +38,9 @@ const CancelAllOrdersButton = ({
}) => {
const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={() => onClick(marketId)}
data-testid="cancelAll"
@ -107,24 +63,48 @@ export const OrderListManager = ({
const [filter, setFilter] = useState<Filter | undefined>();
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows, reload } =
useOrderListData({
partyId,
marketId,
sort,
filter,
gridRef,
scrolledToTop,
});
const {
data,
error,
loading,
addNewRows,
getRows,
reload,
makeBottomPlaceholders,
} = useOrderListData({
partyId,
marketId,
sort,
filter,
gridRef,
scrolledToTop,
});
const checkBottomPlaceholder = useCallback(() => {
if (!isReadOnly && hasActiveOrder) {
const rowCont = gridRef.current?.api?.getModel().getRowCount() ?? 0;
const lastRowIndex = gridRef.current?.api?.getLastDisplayedRow();
if (lastRowIndex && rowCont - 1 === lastRowIndex) {
const lastrow =
gridRef.current?.api.getDisplayedRowAtIndex(lastRowIndex);
lastrow?.setRowHeight(50);
makeBottomPlaceholders(lastrow?.data);
gridRef.current?.api.onRowHeightChanged();
gridRef.current?.api.refreshInfiniteCache();
}
}
}, [isReadOnly, hasActiveOrder, makeBottomPlaceholders]);
const onBodyScrollEnd = useCallback(
(event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
checkBottomPlaceholder();
},
[addNewRows]
[addNewRows, checkBottomPlaceholder]
);
const onBodyScroll = useCallback((event: BodyScrollEvent) => {
@ -133,18 +113,21 @@ export const OrderListManager = ({
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
makeBottomPlaceholders();
const updatedFilter = event.api.getFilterModel();
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else {
setFilter(undefined);
}
checkBottomPlaceholder();
},
[setFilter]
[setFilter, makeBottomPlaceholders, checkBottomPlaceholder]
);
const onSortChange = useCallback(
(event: SortChangedEvent) => {
makeBottomPlaceholders();
const sort = event.columnApi
.getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
@ -156,8 +139,9 @@ export const OrderListManager = ({
return acc;
}, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined);
checkBottomPlaceholder();
},
[setSort]
[setSort, makeBottomPlaceholders, checkBottomPlaceholder]
);
const cancel = useCallback(
@ -172,7 +156,6 @@ export const OrderListManager = ({
},
[create]
);
const cancelAll = useCallback(
(marketId?: string) => {
create({
@ -183,43 +166,47 @@ export const OrderListManager = ({
},
[create]
);
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules } =
useBottomPlaceholder<Order>({
gridRef,
});
return (
<>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="relative">
<OrderListTable
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
suppressNoRowsOverlay
<div className="h-full relative">
<OrderListTable
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
suppressNoRowsOverlay
isFullWidthRow={isFullWidthRow}
fullWidthCellRenderer={fullWidthCellRenderer}
rowClassRules={rowClassRules}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)}
reload={reload}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)}
reload={reload}
/>
</div>
</div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
)}
</div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
)}
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
@ -257,76 +244,3 @@ export const OrderListManager = ({
</>
);
};
export const getEditDialogTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order updated');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Order amendment failed');
}
};
export const getCancelDialogIntent = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderSubFieldsFragment | null;
transactionResult?: TransactionResult;
}): Intent | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return Intent.Success;
}
return Intent.Danger;
}
if (transactionResult) {
if ('error' in transactionResult && transactionResult.error) {
return Intent.Danger;
}
return Intent.Success;
}
return;
};
export const getCancelDialogTitle = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderSubFieldsFragment | null;
transactionResult?: TransactionResult;
}): string | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return t('Order cancelled');
}
return t('Order cancellation failed');
}
if (transactionResult) {
if (transactionResult.status) {
return t('Orders cancelled');
}
return t('Orders not cancelled');
}
return;
};

View File

@ -51,6 +51,21 @@ export const useOrderListData = ({
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<
OrdersQueryVariables & OrdersUpdateSubscriptionVariables
@ -130,5 +145,13 @@ export const useOrderListData = ({
load,
newRows
);
return { loading, error, data, addNewRows, getRows, reload };
return {
loading,
error,
data,
addNewRows,
getRows,
reload,
makeBottomPlaceholders,
};
};

View File

@ -8,7 +8,7 @@ import * as Schema from '@vegaprotocol/types';
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import BigNumber from 'bignumber.js';
import { forwardRef } from 'react';
import { memo, forwardRef } from 'react';
import {
AgGridDynamic as AgGrid,
SetFilter,
@ -33,257 +33,262 @@ export type OrderListTableProps = OrderListProps & {
isReadOnly: boolean;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => {
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No orders')}
defaultColDef={{
flex: 1,
resizable: true,
filterParams: { buttons: ['reset'] },
}}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
cellRenderer={({
value,
data,
}: VegaICellRendererParams<
Order,
'market.tradableInstrument.instrument.code'
>) =>
onMarketClick ? (
<Link
onClick={() =>
data?.market?.id && onMarketClick(data?.market?.id)
}
>
{value}
</Link>
) : (
value
)
}
/>
<AgGridColumn
headerName={t('Size')}
field="size"
cellClass="font-mono text-right"
type="rightAligned"
cellClassRules={{
[positiveClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_SELL,
export const OrderListTable = memo(
forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => {
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No orders')}
defaultColDef={{
flex: 1,
resizable: true,
filterParams: { buttons: ['reset'] },
}}
valueFormatter={({
value,
data,
node,
}: VegaValueFormatterParams<Order, 'size'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value)) {
return '-';
}
const prefix = data
? data.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(value, data.market.positionDecimalPlaces)
);
style={{
width: '100%',
height: '100%',
}}
/>
<AgGridColumn
field="type"
filter={SetFilter}
filterParams={{
set: Schema.OrderTypeMapping,
}}
valueFormatter={({
data: order,
value,
node,
}: VegaValueFormatterParams<Order, 'type'>) => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) return t('Pegged');
if (order?.liquidityProvision) return t('Liquidity provision');
return Schema.OrderTypeMapping[value];
}}
/>
<AgGridColumn
field="status"
filter={SetFilter}
filterParams={{
set: Schema.OrderStatusMapping,
}}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Order, 'status'>) => {
if (data?.rejectionReason && value) {
return `${Schema.OrderStatusMapping[value]}: ${
data?.rejectionReason &&
Schema.OrderRejectionReasonMapping[data.rejectionReason]
}`;
}
return value ? Schema.OrderStatusMapping[value] : '';
}}
cellRenderer={({
valueFormatted,
data,
}: {
valueFormatted: string;
data: Order;
}) => (
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
)}
/>
<AgGridColumn
headerName={t('Filled')}
field="remaining"
cellClass="font-mono text-right"
type="rightAligned"
valueFormatter={({
data,
value,
node,
}: VegaValueFormatterParams<Order, 'remaining'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
return '-';
}
const dps = data.market.positionDecimalPlaces;
const size = new BigNumber(data.size);
const remaining = new BigNumber(value);
const fills = size.minus(remaining);
return `${addDecimalsFormatNumber(
fills.toString(),
dps
)}/${addDecimalsFormatNumber(size.toString(), dps)}`;
}}
/>
<AgGridColumn
field="price"
type="rightAligned"
cellClass="font-mono text-right"
valueFormatter={({
value,
data,
node,
}: VegaValueFormatterParams<Order, 'price'>) => {
if (!data) {
return undefined;
}
if (
!data?.market ||
data.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
field="timeInForce"
filter={SetFilter}
filterParams={{
set: Schema.OrderTimeInForceMapping,
}}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Order, 'timeInForce'>) => {
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
data?.expiresAt
) {
const expiry = getDateTimeFormat().format(
new Date(data.expiresAt)
);
return `${Schema.OrderTimeInForceMapping[value]}: ${expiry}`;
}
return value ? Schema.OrderTimeInForceMapping[value] : '';
}}
/>
<AgGridColumn
field="createdAt"
cellRenderer={({
data,
value,
}: VegaICellRendererParams<Order, 'createdAt'>) => {
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : value}
</span>
);
}}
/>
<AgGridColumn
field="updatedAt"
filter={DateRangeFilter}
cellRenderer={({
data,
value,
}: VegaICellRendererParams<Order, 'updatedAt'>) => {
if (!data) {
return undefined;
}
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
}}
/>
<AgGridColumn
colId="amend"
headerName=""
field="status"
minWidth={100}
type="rightAligned"
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
return data && isOrderAmendable(data) && !props.isReadOnly ? (
<>
<ButtonLink
data-testid="edit"
onClick={() => setEditOrder(data)}
getRowId={({ data }) => data.id}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
cellRenderer={({
value,
data,
}: VegaICellRendererParams<
Order,
'market.tradableInstrument.instrument.code'
>) =>
onMarketClick ? (
<Link
onClick={() =>
data?.market?.id && onMarketClick(data?.market?.id)
}
>
{t('Edit')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink data-testid="cancel" onClick={() => cancel(data)}>
{t('Cancel')}
</ButtonLink>
</>
) : null;
}}
/>
</AgGrid>
);
}
{value}
</Link>
) : (
value
)
}
/>
<AgGridColumn
headerName={t('Size')}
field="size"
cellClass="font-mono text-right"
type="rightAligned"
cellClassRules={{
[positiveClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_SELL,
}}
valueFormatter={({
value,
data,
node,
}: VegaValueFormatterParams<Order, 'size'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value)) {
return '-';
}
const prefix = data
? data.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
value,
data.market.positionDecimalPlaces
)
);
}}
/>
<AgGridColumn
field="type"
filter={SetFilter}
filterParams={{
set: Schema.OrderTypeMapping,
}}
valueFormatter={({
data: order,
value,
node,
}: VegaValueFormatterParams<Order, 'type'>) => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) return t('Pegged');
if (order?.liquidityProvision) return t('Liquidity provision');
return Schema.OrderTypeMapping[value];
}}
/>
<AgGridColumn
field="status"
filter={SetFilter}
filterParams={{
set: Schema.OrderStatusMapping,
}}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Order, 'status'>) => {
if (data?.rejectionReason && value) {
return `${Schema.OrderStatusMapping[value]}: ${
data?.rejectionReason &&
Schema.OrderRejectionReasonMapping[data.rejectionReason]
}`;
}
return value ? Schema.OrderStatusMapping[value] : '';
}}
cellRenderer={({
valueFormatted,
data,
}: {
valueFormatted: string;
data: Order;
}) => (
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
)}
/>
<AgGridColumn
headerName={t('Filled')}
field="remaining"
cellClass="font-mono text-right"
type="rightAligned"
valueFormatter={({
data,
value,
node,
}: VegaValueFormatterParams<Order, 'remaining'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
return '-';
}
const dps = data.market.positionDecimalPlaces;
const size = new BigNumber(data.size);
const remaining = new BigNumber(value);
const fills = size.minus(remaining);
return `${addDecimalsFormatNumber(
fills.toString(),
dps
)}/${addDecimalsFormatNumber(size.toString(), dps)}`;
}}
/>
<AgGridColumn
field="price"
type="rightAligned"
cellClass="font-mono text-right"
valueFormatter={({
value,
data,
node,
}: VegaValueFormatterParams<Order, 'price'>) => {
if (!data) {
return undefined;
}
if (
!data?.market ||
data.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
field="timeInForce"
filter={SetFilter}
filterParams={{
set: Schema.OrderTimeInForceMapping,
}}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Order, 'timeInForce'>) => {
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
data?.expiresAt
) {
const expiry = getDateTimeFormat().format(
new Date(data.expiresAt)
);
return `${Schema.OrderTimeInForceMapping[value]}: ${expiry}`;
}
return value ? Schema.OrderTimeInForceMapping[value] : '';
}}
/>
<AgGridColumn
field="createdAt"
cellRenderer={({
data,
value,
}: VegaICellRendererParams<Order, 'createdAt'>) => {
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : value}
</span>
);
}}
/>
<AgGridColumn
field="updatedAt"
filter={DateRangeFilter}
cellRenderer={({
data,
value,
}: VegaICellRendererParams<Order, 'updatedAt'>) => {
if (!data) {
return undefined;
}
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
}}
/>
<AgGridColumn
colId="amend"
headerName=""
field="status"
minWidth={100}
type="rightAligned"
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
return data && isOrderAmendable(data) && !props.isReadOnly ? (
<>
<ButtonLink
data-testid="edit"
onClick={() => setEditOrder(data)}
>
{t('Edit')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink data-testid="cancel" onClick={() => cancel(data)}>
{t('Cancel')}
</ButtonLink>
</>
) : null;
}}
/>
</AgGrid>
);
}
)
);
/**

View File

@ -7,8 +7,7 @@ export const useHasActiveOrder = (marketId?: string) => {
const { pubKey } = useVegaWallet();
const [hasActiveOrder, setHasActiveOrder] = useState(false);
const update = useCallback(({ data }: { data: boolean | null }) => {
console.log({ data });
setHasActiveOrder(!!data);
setHasActiveOrder(Boolean(data));
return true;
}, []);
useDataProvider({

View File

@ -16,3 +16,4 @@ export * from './use-storybook-theme-observer';
export * from './use-yesterday';
export * from './use-previous';
export * from './use-logger';
export * from './use-bottom-placeholder';

View File

@ -0,0 +1,71 @@
import type { RefObject } from 'react';
import { useCallback, useMemo } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { IsFullWidthRowParams } from 'ag-grid-community';
const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' };
const fullWidthCellRenderer = () => null;
const isFullWidthRow = (params: IsFullWidthRowParams) =>
params.rowNode.data?.isLastPlaceholder;
interface Props<T> {
gridRef: RefObject<AgGridReact>;
setId?: (data: T) => T;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = <T extends {}>({
gridRef,
setId,
}: 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 })
: {
...lastRow.data,
isLastPlaceholder: true,
id: `${lastRow.data?.id || '-'}-1`,
};
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();
}
}
}, [gridRef, setId]);
const onRowsChanged = useCallback(() => {
const remove: T[] = [];
gridRef.current?.api.forEachNodeAfterFilterAndSort((rowNode) => {
if (rowNode.data.isLastPlaceholder) {
remove.push(rowNode.data);
}
});
gridRef.current?.api.applyTransaction({
remove,
});
onBodyScrollEnd();
}, [gridRef, onBodyScrollEnd]);
return useMemo(
() => ({
onBodyScrollEnd,
rowClassRules: NO_HOVER_CSS_RULE,
isFullWidthRow,
fullWidthCellRenderer,
onSortChanged: onRowsChanged,
onFilterChange: onRowsChanged,
}),
[onBodyScrollEnd, onRowsChanged]
);
};

View File

@ -1,4 +1,4 @@
import { useThemeSwitcher } from '@vegaprotocol/utils';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import { useEffect } from 'react';
import '../src/styles.css';

View File

@ -1,3 +1,5 @@
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import {
getDateTimeFormat,
@ -5,14 +7,9 @@ import {
addDecimalsFormatNumber,
isNumeric,
} from '@vegaprotocol/utils';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/i18n';
import {
Link,
ButtonLink,
Intent,
Icon,
Loader,
} from '@vegaprotocol/ui-toolkit';
import { Link, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
import type {
TypedDataAgGrid,
@ -29,11 +26,13 @@ import { ApprovalStatus } from './use-verify-withdrawal';
export const WithdrawalsTable = (
props: TypedDataAgGrid<WithdrawalFieldsFragment>
) => {
const gridRef = useRef<AgGridReact | null>(null);
const { ETHERSCAN_URL } = useEnvironment();
const createWithdrawApproval = useEthWithdrawApprovalsStore(
(store) => store.create
);
const bottomPlaceholderProps = useBottomPlaceholder({ gridRef });
return (
<AgGrid
overlayNoRowsTemplate={t('No withdrawals')}
@ -45,8 +44,9 @@ export const WithdrawalsTable = (
EtherscanLinkCell,
CompleteCell,
}}
suppressCellFocus={true}
domLayout="autoHeight"
suppressCellFocus
ref={gridRef}
{...bottomPlaceholderProps}
{...props}
>
<AgGridColumn headerName="Asset" field="asset.symbol" />
@ -69,10 +69,12 @@ export const WithdrawalsTable = (
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'details.receiverAddress'
>) => {
if (!data) return null;
if (!value) return '-';
return truncateByChars(value);
}}
@ -82,20 +84,34 @@ export const WithdrawalsTable = (
field="createdTimestamp"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'createdTimestamp'
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
>) =>
data
? value
? getDateTimeFormat().format(new Date(value))
: '-'
: null
}
/>
<AgGridColumn
headerName={t('Completed')}
field="withdrawnTimestamp"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'withdrawnTimestamp'
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
>) =>
data
? value
? getDateTimeFormat().format(new Date(value))
: '-'
: null
}
/>
<AgGridColumn
headerName={t('Status')}
@ -126,8 +142,11 @@ export type CompleteCellProps = {
data: WithdrawalFieldsFragment;
complete: (withdrawal: WithdrawalFieldsFragment) => void;
};
export const CompleteCell = ({ data, complete }: CompleteCellProps) =>
data.pendingOnForeignChain ? (
export const CompleteCell = ({ data, complete }: CompleteCellProps) => {
if (!data) {
return null;
}
return data.pendingOnForeignChain ? (
'-'
) : (
<ButtonLink
@ -137,6 +156,7 @@ export const CompleteCell = ({ data, complete }: CompleteCellProps) =>
{t('Complete withdrawal')}
</ButtonLink>
);
};
export const EtherscanLinkCell = ({
value,
@ -158,6 +178,9 @@ export const EtherscanLinkCell = ({
};
export const StatusCell = ({ data }: { data: WithdrawalFieldsFragment }) => {
if (!data) {
return null;
}
if (data.pendingOnForeignChain || !data.txHash) {
return <span>{t('Pending')}</span>;
}
@ -195,25 +218,6 @@ const RecipientCell = ({
);
};
export const getVerifyDialogProps = (status: ApprovalStatus) => {
if (status === ApprovalStatus.Error) {
return {
intent: Intent.Danger,
icon: <Icon name="warning-sign" />,
};
}
if (status === ApprovalStatus.Pending) {
return { intent: Intent.None, icon: <Loader size="small" /> };
}
if (status === ApprovalStatus.Delayed) {
return { intent: Intent.Warning, icon: <Icon name="time" /> };
}
return { intent: Intent.None };
};
export const VerificationStatus = ({ state }: { state: VerifyState }) => {
if (state.status === ApprovalStatus.Error) {
return <p>{state.message || t('Something went wrong')}</p>;