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 { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits'; import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import {
useDataProvider,
useBottomPlaceholder,
} from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
export const DepositsContainer = () => { export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error, reload } = useDataProvider({ const { data, loading, error, reload } = useDataProvider({
dataProvider: depositsProvider, dataProvider: depositsProvider,
@ -13,13 +19,15 @@ export const DepositsContainer = () => {
skip: !pubKey, skip: !pubKey,
}); });
const openDepositDialog = useDepositDialog((state) => state.open); const openDepositDialog = useDepositDialog((state) => state.open);
const bottomPlaceholderProps = useBottomPlaceholder({ gridRef });
return ( return (
<div className="h-full grid grid-rows-[1fr,min-content]"> <div className="h-full">
<div className="h-full relative"> <div className="h-full relative">
<DepositsTable <DepositsTable
rowData={data || []} rowData={data || []}
noRowsOverlayComponent={() => null} noRowsOverlayComponent={() => null}
ref={gridRef}
{...bottomPlaceholderProps}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<AsyncRenderer <AsyncRenderer
@ -33,8 +41,9 @@ export const DepositsContainer = () => {
</div> </div>
</div> </div>
{!isReadOnly && ( {!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 <Button
variant="primary"
size="sm" size="sm"
onClick={() => openDepositDialog()} onClick={() => openDepositDialog()}
data-testid="deposit-button" data-testid="deposit-button"

View File

@ -20,36 +20,35 @@ export const WithdrawalsContainer = () => {
return ( return (
<VegaWalletContainer> <VegaWalletContainer>
<div className="h-full relative grid grid-rows-[1fr,min-content]"> <div className="h-full relative">
<div className="h-full relative"> <WithdrawalsTable
<WithdrawalsTable data-testid="withdrawals-history"
data-testid="withdrawals-history" rowData={data}
rowData={data} noRowsOverlayComponent={() => null}
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> </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> </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> </VegaWalletContainer>
); );
}; };

View File

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

View File

@ -14,7 +14,7 @@ export const Footer = () => {
const { blockDiff, datanodeBlockHeight } = useNodeHealth(); const { blockDiff, datanodeBlockHeight } = useNodeHealth();
return ( 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 */} {/* Pull left to align with top nav, due to button padding */}
<div className="-ml-2"> <div className="-ml-2">
{VEGA_URL && ( {VEGA_URL && (

View File

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

View File

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

View File

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

View File

@ -22,13 +22,15 @@ const agGridDarkVariables = `
border-width: 1px 0; border-width: 1px 0;
border-bottom: 1px solid transparent; 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 { .ag-theme-balham-dark .ag-react-container {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ag-theme-balham-dark .ag-cell, .ag-theme-balham-dark .ag-full-width-row .ag-cell-wrapper.ag-row-group { .ag-theme-balham-dark .ag-cell, .ag-theme-balham-dark .ag-full-width-row .ag-cell-wrapper.ag-row-group {
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px); line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
} }
`; `;

View File

@ -22,13 +22,15 @@ const agGridLightVariables = `
border-width: 1px 0; border-width: 1px 0;
border-bottom: 1px solid transparent; 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 { .ag-theme-balham .ag-react-container {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ag-theme-balham .ag-cell, .ag-theme-balham .ag-full-width-row .ag-cell-wrapper.ag-row-group { .ag-theme-balham .ag-cell, .ag-theme-balham .ag-full-width-row .ag-cell-wrapper.ag-row-group {
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px); line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
} }
`; `;

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" field="txHash"
cellRenderer={({ cellRenderer={({
value, value,
data,
}: VegaICellRendererParams<DepositFieldsFragment, 'txHash'>) => { }: VegaICellRendererParams<DepositFieldsFragment, 'txHash'>) => {
if (!data) return null;
if (!value) return '-'; if (!value) return '-';
return ( return (
<Link <Link

View File

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

View File

@ -1,5 +1,4 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { import type {
@ -8,28 +7,19 @@ import type {
FilterChangedEvent, FilterChangedEvent,
SortChangedEvent, SortChangedEvent,
} from 'ag-grid-community'; } 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 type { AgGridReact } from 'ag-grid-react';
import { OrderListTable } from '../order-list/order-list'; import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data'; import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order'; import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import type { Filter, Sort } from './use-order-list-data'; import type { Filter, Sort } from './use-order-list-data';
import { useEnvironment } from '@vegaprotocol/environment'; import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { Link } from '@vegaprotocol/ui-toolkit';
import { import {
normalizeOrderAmendment, normalizeOrderAmendment,
useVegaTransactionStore, useVegaTransactionStore,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import type { import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
VegaTxState,
TransactionResult,
OrderTxUpdateFieldsFragment,
} from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog'; 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'; import type { Order } from '../order-data-provider';
export interface OrderListManagerProps { export interface OrderListManagerProps {
@ -39,41 +29,6 @@ export interface OrderListManagerProps {
isReadOnly: boolean; 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 = ({ const CancelAllOrdersButton = ({
onClick, onClick,
marketId, marketId,
@ -83,8 +38,9 @@ const CancelAllOrdersButton = ({
}) => { }) => {
const hasActiveOrder = useHasActiveOrder(marketId); const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? ( 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 <Button
variant="primary"
size="sm" size="sm"
onClick={() => onClick(marketId)} onClick={() => onClick(marketId)}
data-testid="cancelAll" data-testid="cancelAll"
@ -107,24 +63,48 @@ export const OrderListManager = ({
const [filter, setFilter] = useState<Filter | undefined>(); const [filter, setFilter] = useState<Filter | undefined>();
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 hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows, reload } = const {
useOrderListData({ data,
partyId, error,
marketId, loading,
sort, addNewRows,
filter, getRows,
gridRef, reload,
scrolledToTop, 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( const onBodyScrollEnd = useCallback(
(event: BodyScrollEndEvent) => { (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {
addNewRows(); addNewRows();
} }
checkBottomPlaceholder();
}, },
[addNewRows] [addNewRows, checkBottomPlaceholder]
); );
const onBodyScroll = useCallback((event: BodyScrollEvent) => { const onBodyScroll = useCallback((event: BodyScrollEvent) => {
@ -133,18 +113,21 @@ export const OrderListManager = ({
const onFilterChanged = useCallback( const onFilterChanged = useCallback(
(event: FilterChangedEvent) => { (event: FilterChangedEvent) => {
makeBottomPlaceholders();
const updatedFilter = event.api.getFilterModel(); const updatedFilter = event.api.getFilterModel();
if (Object.keys(updatedFilter).length) { if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter); setFilter(updatedFilter);
} else { } else {
setFilter(undefined); setFilter(undefined);
} }
checkBottomPlaceholder();
}, },
[setFilter] [setFilter, makeBottomPlaceholders, checkBottomPlaceholder]
); );
const onSortChange = useCallback( const onSortChange = useCallback(
(event: SortChangedEvent) => { (event: SortChangedEvent) => {
makeBottomPlaceholders();
const sort = event.columnApi const sort = event.columnApi
.getColumnState() .getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0)) .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
@ -156,8 +139,9 @@ export const OrderListManager = ({
return acc; return acc;
}, [] as { colId: string; sort: string }[]); }, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined); setSort(sort.length > 0 ? sort : undefined);
checkBottomPlaceholder();
}, },
[setSort] [setSort, makeBottomPlaceholders, checkBottomPlaceholder]
); );
const cancel = useCallback( const cancel = useCallback(
@ -172,7 +156,6 @@ export const OrderListManager = ({
}, },
[create] [create]
); );
const cancelAll = useCallback( const cancelAll = useCallback(
(marketId?: string) => { (marketId?: string) => {
create({ create({
@ -183,43 +166,47 @@ export const OrderListManager = ({
}, },
[create] [create]
); );
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules } =
useBottomPlaceholder<Order>({
gridRef,
});
return ( return (
<> <>
<div className="h-full relative grid grid-rows-[1fr,min-content]"> <div className="h-full relative">
<div className="relative"> <OrderListTable
<OrderListTable ref={gridRef}
ref={gridRef} rowModelType="infinite"
rowModelType="infinite" datasource={{ getRows }}
datasource={{ getRows }} onBodyScrollEnd={onBodyScrollEnd}
onBodyScrollEnd={onBodyScrollEnd} onBodyScroll={onBodyScroll}
onBodyScroll={onBodyScroll} onFilterChanged={onFilterChanged}
onFilterChanged={onFilterChanged} onSortChanged={onSortChange}
onSortChanged={onSortChange} cancel={cancel}
cancel={cancel} setEditOrder={setEditOrder}
setEditOrder={setEditOrder} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} isReadOnly={isReadOnly}
isReadOnly={isReadOnly} blockLoadDebounceMillis={100}
blockLoadDebounceMillis={100} suppressLoadingOverlay
suppressLoadingOverlay suppressNoRowsOverlay
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> </div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
)}
</div> </div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
)}
{editOrder && ( {editOrder && (
<OrderEditDialog <OrderEditDialog
isOpen={Boolean(editOrder)} 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 dataRef = useRef<(OrderEdge | null)[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined); const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0); 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< const variables = useMemo<
OrdersQueryVariables & OrdersUpdateSubscriptionVariables OrdersQueryVariables & OrdersUpdateSubscriptionVariables
@ -130,5 +145,13 @@ export const useOrderListData = ({
load, load,
newRows 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 { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { forwardRef } from 'react'; import { memo, forwardRef } from 'react';
import { import {
AgGridDynamic as AgGrid, AgGridDynamic as AgGrid,
SetFilter, SetFilter,
@ -33,257 +33,262 @@ export type OrderListTableProps = OrderListProps & {
isReadOnly: boolean; isReadOnly: boolean;
}; };
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>( export const OrderListTable = memo(
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => { forwardRef<AgGridReact, OrderListTableProps>(
return ( ({ cancel, setEditOrder, onMarketClick, ...props }, ref) => {
<AgGrid return (
ref={ref} <AgGrid
overlayNoRowsTemplate={t('No orders')} ref={ref}
defaultColDef={{ overlayNoRowsTemplate={t('No orders')}
flex: 1, defaultColDef={{
resizable: true, flex: 1,
filterParams: { buttons: ['reset'] }, 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,
}} }}
valueFormatter={({ style={{
value, width: '100%',
data, height: '100%',
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)
);
}} }}
/> getRowId={({ data }) => data.id}
<AgGridColumn {...props}
field="type" >
filter={SetFilter} <AgGridColumn
filterParams={{ headerName={t('Market')}
set: Schema.OrderTypeMapping, field="market.tradableInstrument.instrument.code"
}} cellRenderer={({
valueFormatter={({ value,
data: order, data,
value, }: VegaICellRendererParams<
node, Order,
}: VegaValueFormatterParams<Order, 'type'>) => { 'market.tradableInstrument.instrument.code'
if (!order) { >) =>
return undefined; onMarketClick ? (
} <Link
if (!value) return '-'; onClick={() =>
if (order?.peggedOrder) return t('Pegged'); data?.market?.id && onMarketClick(data?.market?.id)
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')} {value}
</ButtonLink> </Link>
<span className="mx-1" /> ) : (
<ButtonLink data-testid="cancel" onClick={() => cancel(data)}> value
{t('Cancel')} )
</ButtonLink> }
</> />
) : null; <AgGridColumn
}} headerName={t('Size')}
/> field="size"
</AgGrid> 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 { pubKey } = useVegaWallet();
const [hasActiveOrder, setHasActiveOrder] = useState(false); const [hasActiveOrder, setHasActiveOrder] = useState(false);
const update = useCallback(({ data }: { data: boolean | null }) => { const update = useCallback(({ data }: { data: boolean | null }) => {
console.log({ data }); setHasActiveOrder(Boolean(data));
setHasActiveOrder(!!data);
return true; return true;
}, []); }, []);
useDataProvider({ useDataProvider({

View File

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