feat(2408): trading data grid snags (#2513)
* feat: trading data grid snags * feat: fix e2e tests, fix use order list data avoid rerender condition
This commit is contained in:
parent
1f96ccea68
commit
4608683bde
@ -66,7 +66,7 @@ describe('Portfolio page tabs', { tags: '@smoke' }, () => {
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||
cy.get('[title="tEURO"] button').click();
|
||||
cy.contains('.ag-center-cols-container button', 'tEURO').click();
|
||||
cy.getByTestId('dialog-title').should(
|
||||
'have.text',
|
||||
'Asset details - tEURO'
|
||||
|
@ -41,7 +41,7 @@ const AccountsManager = () => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
const lastRow = dataRef.current ? dataRef.current.length : 0;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
};
|
||||
const { columnDefs, defaultColDef } = useAccountColumnDefinitions();
|
||||
|
@ -5,7 +5,6 @@ import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import {
|
||||
PendingWithdrawalsTable,
|
||||
useWithdrawals,
|
||||
useWithdrawalDialog,
|
||||
WithdrawalsTable,
|
||||
@ -30,7 +29,7 @@ const Withdrawals = ({ name }: RouteChildProps) => {
|
||||
const WithdrawPendingContainer = () => {
|
||||
const openWithdrawalDialog = useWithdrawalDialog((state) => state.open);
|
||||
const { t } = useTranslation();
|
||||
const { pending, completed, loading, error } = useWithdrawals();
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@ -60,11 +59,7 @@ const WithdrawPendingContainer = () => {
|
||||
<p>{t('withdrawalsText')}</p>
|
||||
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
|
||||
<div className="w-full h-[500px]">
|
||||
{pending && pending.length > 0 && (
|
||||
<PendingWithdrawalsTable rowData={pending} />
|
||||
)}
|
||||
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
|
||||
<WithdrawalsTable rowData={completed} />
|
||||
<WithdrawalsTable rowData={data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -20,9 +20,19 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="breakdown"]')
|
||||
.find('[col-id="breakdown"] [data-testid="breakdown"]')
|
||||
.should('have.text', 'Breakdown');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="breakdown"] [data-testid="deposit"]')
|
||||
.should('have.text', 'Deposit');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="breakdown"] [data-testid="withdraw"]')
|
||||
.should('have.text', 'Withdraw');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="deposited"]')
|
||||
|
@ -16,7 +16,11 @@ describe('Portfolio page', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('"Ledger entries"').click();
|
||||
const headers = [
|
||||
'Sender',
|
||||
'Account type',
|
||||
'Market',
|
||||
'Receiver',
|
||||
'Account type',
|
||||
'Market',
|
||||
'Transfer Type',
|
||||
'Quantity',
|
||||
'Asset',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
PendingWithdrawalsTable,
|
||||
useWithdrawals,
|
||||
useWithdrawalDialog,
|
||||
WithdrawalsTable,
|
||||
@ -9,35 +8,27 @@ import { t } from '@vegaprotocol/react-helpers';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
|
||||
export const WithdrawalsContainer = () => {
|
||||
const { pending, completed, loading, error } = useWithdrawals();
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
|
||||
|
||||
return (
|
||||
<VegaWalletContainer>
|
||||
<div className="h-full relative grid grid-rows-[1fr,min-content]">
|
||||
<div className="h-full">
|
||||
<AsyncRenderer
|
||||
data={{ pending, completed }}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={({ pending, completed }) => (
|
||||
<>
|
||||
{pending && pending.length > 0 && (
|
||||
<>
|
||||
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
|
||||
<PendingWithdrawalsTable rowData={pending} />
|
||||
</>
|
||||
)}
|
||||
{completed && completed.length > 0 && (
|
||||
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
|
||||
)}
|
||||
<WithdrawalsTable
|
||||
data-testid="withdrawals-history"
|
||||
rowData={completed}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<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')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
|
||||
<Button
|
||||
|
@ -43,7 +43,7 @@ export const AccountManager = ({
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? undefined;
|
||||
const lastRow = dataRef.current ? dataRef.current.length : 0;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
},
|
||||
[]
|
||||
@ -58,9 +58,10 @@ export const AccountManager = ({
|
||||
onClickDeposit={onClickDeposit}
|
||||
onClickWithdraw={onClickWithdraw}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 top-5">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
data={data?.length ? data : null}
|
||||
data={data}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
error={error}
|
||||
loading={loading}
|
||||
noDataMessage={t('No accounts')}
|
||||
|
@ -38,7 +38,7 @@ describe('AccountsTable', () => {
|
||||
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
|
||||
);
|
||||
});
|
||||
const expectedHeaders = ['Asset', 'Total', 'Used', 'Available', '', ''];
|
||||
const expectedHeaders = ['Asset', 'Total', 'Used', 'Available', ''];
|
||||
const headers = await screen.findAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
@ -42,7 +42,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
overlayNoRowsTemplate={t('No accounts')}
|
||||
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
|
||||
ref={ref}
|
||||
rowHeight={34}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
@ -64,7 +63,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
|
||||
return value ? (
|
||||
<ButtonLink
|
||||
data-testid="deposit"
|
||||
data-testid="asset"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
onClickAsset(data.asset.id);
|
||||
@ -129,55 +128,44 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
maxWidth={300}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
field="breakdown"
|
||||
minWidth={150}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<AccountFields, 'breakdown'>) => {
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
setOpenBreakdown(!openBreakdown);
|
||||
setBreakdown(value || null);
|
||||
}}
|
||||
>
|
||||
{t('Breakdown')}
|
||||
</ButtonLink>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="transact"
|
||||
colId="breakdown"
|
||||
headerName=""
|
||||
sortable={false}
|
||||
minWidth={250}
|
||||
minWidth={200}
|
||||
type="rightAligned"
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields>) => {
|
||||
return data ? (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
<>
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
setOpenBreakdown(!openBreakdown);
|
||||
setBreakdown(data.breakdown || null);
|
||||
}}
|
||||
>
|
||||
{t('Breakdown')}
|
||||
</ButtonLink>
|
||||
<span className="mx-1" />
|
||||
<ButtonLink
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
</ButtonLink>
|
||||
<span className="mx-1" />
|
||||
<ButtonLink
|
||||
data-testid="withdraw"
|
||||
onClick={() =>
|
||||
onClickWithdraw && onClickWithdraw(data.asset.id)
|
||||
}
|
||||
>
|
||||
{t('Withdraw')}
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonLink>
|
||||
</>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
|
@ -28,7 +28,7 @@ export const accountsQuery = (
|
||||
return merge(defaultAccounts, override);
|
||||
};
|
||||
|
||||
const accountFields: AccountFieldsFragment[] = [
|
||||
export const accountFields: AccountFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||
import { useFillsList } from './use-fills-list';
|
||||
@ -31,21 +32,25 @@ export const FillsManager = ({ partyId, marketId }: FillsManagerProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataCondition={() => false}
|
||||
>
|
||||
<div className="h-full relative">
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
partyId={partyId}
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No fills')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -54,8 +54,12 @@ export const useFillsList = ({
|
||||
}
|
||||
}
|
||||
dataRef.current = data;
|
||||
const avoidRerender = !!(
|
||||
(dataRef.current?.length && data?.length) ||
|
||||
(!dataRef.current?.length && !data?.length)
|
||||
);
|
||||
gridRef.current?.api?.refreshInfiniteCache();
|
||||
return true;
|
||||
return avoidRerender;
|
||||
}
|
||||
dataRef.current = data;
|
||||
return false;
|
||||
|
@ -34,14 +34,14 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
<LedgerTable
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 top-5">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
@ -50,6 +50,6 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
VegaICellRendererParams,
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
@ -35,58 +34,6 @@ export const TransferTooltipCellComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
type LedgerCellRendererProps = {
|
||||
accountType?: Types.AccountType | null;
|
||||
partyId?: string | null;
|
||||
marketName?: string;
|
||||
};
|
||||
|
||||
const LedgerCellRenderer = ({
|
||||
accountType,
|
||||
partyId,
|
||||
marketName,
|
||||
}: LedgerCellRendererProps) => {
|
||||
return (
|
||||
<div className="flex flex-col justify-around leading-5 h-full">
|
||||
<div
|
||||
className="flex"
|
||||
title={`${t('ID')}: ${truncateByChars(partyId || '-')}`}
|
||||
>
|
||||
{truncateByChars(partyId || '')}
|
||||
</div>
|
||||
<div
|
||||
className="flex"
|
||||
title={`${t('Account type')}: ${
|
||||
accountType ? AccountTypeMapping[accountType] : '-'
|
||||
}`}
|
||||
>
|
||||
{accountType && AccountTypeMapping[accountType]}
|
||||
</div>
|
||||
<div className="flex" title={`${t('Market')}: ${marketName || '-'}`}>
|
||||
{marketName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const SenderCellRenderer = ({ data }: VegaICellRendererParams<LedgerEntry>) => {
|
||||
const props = {
|
||||
accountType: data?.senderAccountType,
|
||||
partyId: data?.senderPartyId,
|
||||
marketName: data?.marketSender?.tradableInstrument?.instrument?.code,
|
||||
};
|
||||
return <LedgerCellRenderer {...props} />;
|
||||
};
|
||||
const ReceiverCellRenderer = ({
|
||||
data,
|
||||
}: VegaICellRendererParams<LedgerEntry>) => {
|
||||
const props = {
|
||||
accountType: data?.receiverAccountType,
|
||||
partyId: data?.receiverPartyId,
|
||||
marketName: data?.marketReceiver?.tradableInstrument?.instrument?.code,
|
||||
};
|
||||
return <LedgerCellRenderer {...props} />;
|
||||
};
|
||||
|
||||
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
||||
|
||||
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
@ -95,7 +42,6 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No entries')}
|
||||
rowHeight={70}
|
||||
ref={ref}
|
||||
getRowId={({ data }) => data.id}
|
||||
tooltipShowDelay={500}
|
||||
@ -109,13 +55,59 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Sender')}
|
||||
field="senderPartyId"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'senderPartyId'>) =>
|
||||
truncateByChars(value || '')
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Account type')}
|
||||
field="senderAccountType"
|
||||
cellRenderer={SenderCellRenderer}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'senderAccountType'>) =>
|
||||
value ? AccountTypeMapping[value] : '-'
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketSender.tradableInstrument.instrument.code"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
LedgerEntry,
|
||||
'marketSender.tradableInstrument.instrument.code'
|
||||
>) => value || '-'}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Receiver')}
|
||||
field="receiverPartyId"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'receiverPartyId'>) =>
|
||||
truncateByChars(value || '')
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Account type')}
|
||||
field="receiverAccountType"
|
||||
cellRenderer={ReceiverCellRenderer}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'receiverAccountType'>) =>
|
||||
value ? AccountTypeMapping[value] : '-'
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketReceiver.tradableInstrument.instrument.code"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
LedgerEntry,
|
||||
'marketReceiver.tradableInstrument.instrument.code'
|
||||
>) => value || '-'}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Transfer Type')}
|
||||
|
@ -57,7 +57,6 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No liquidity provisions')}
|
||||
getRowId={({ data }) => getId(data)}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
|
@ -11,7 +11,7 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@ -58,14 +58,13 @@ export const MarketListTable = forwardRef<
|
||||
'tradableInstrument.instrument.product.settlementAsset'
|
||||
>) =>
|
||||
value ? (
|
||||
<button
|
||||
className="hover:underline"
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{value.symbol}
|
||||
</button>
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
|
@ -53,7 +53,7 @@ it('Renders an error state', () => {
|
||||
it('Renders the order list if orders provided', async () => {
|
||||
// @ts-ignore Orderlist is read only but we need to override with the forwardref to
|
||||
// avoid warnings about padding refs
|
||||
orderListMock.OrderList = forwardRef(() => <div>OrderList</div>);
|
||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [{ id: '1' } as OrderFieldsFragment],
|
||||
loading: false,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import { useRef, useState } from 'react';
|
||||
import type {
|
||||
BodyScrollEvent,
|
||||
@ -7,17 +7,66 @@ import type {
|
||||
FilterChangedEvent,
|
||||
SortChangedEvent,
|
||||
} from 'ag-grid-community';
|
||||
import { Button, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
|
||||
import { OrderList } from '../order-list/order-list';
|
||||
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 type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
|
||||
import { useOrderEdit } from '../../order-hooks/use-order-edit';
|
||||
import { OrderFeedback } from '../order-feedback';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { OrderEventFieldsFragment } from '../../order-hooks';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Order } from '../order-data-provider';
|
||||
|
||||
export interface OrderListManagerProps {
|
||||
partyId: string;
|
||||
marketId?: string;
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrderListManager = ({
|
||||
partyId,
|
||||
marketId,
|
||||
@ -26,6 +75,10 @@ export const OrderListManager = ({
|
||||
const scrolledToTop = useRef(true);
|
||||
const [sort, setSort] = useState<Sort[] | undefined>();
|
||||
const [filter, setFilter] = useState<Filter | undefined>();
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
const orderCancel = useOrderCancel();
|
||||
const orderEdit = useOrderEdit(editOrder);
|
||||
const hasActiveOrder = useHasActiveOrder(marketId);
|
||||
|
||||
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||
partyId,
|
||||
@ -71,25 +124,158 @@ export const OrderListManager = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrderList
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
onFilterChanged={onFilterChanged}
|
||||
onSortChanged={onSortChange}
|
||||
marketId={marketId}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 top-5">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No orders')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
<div className="h-full relative grid grid-rows-[1fr,min-content]">
|
||||
<div className="h-full relative">
|
||||
<OrderListTable
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
onFilterChanged={onFilterChanged}
|
||||
onSortChanged={onSortChange}
|
||||
cancel={(order: Order) => {
|
||||
if (!order.market) return;
|
||||
orderCancel.cancel({
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
});
|
||||
}}
|
||||
setEditOrder={setEditOrder}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No orders')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hasActiveOrder && (
|
||||
<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={() => orderCancel.cancel({ marketId })}
|
||||
data-testid="cancelAll"
|
||||
>
|
||||
{t('Cancel all')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel)}
|
||||
intent={getCancelDialogIntent(orderCancel)}
|
||||
content={{
|
||||
Complete: orderCancel.cancelledOrder ? (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
) : (
|
||||
<TransactionComplete {...orderCancel} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) setEditOrder(null);
|
||||
}}
|
||||
order={editOrder}
|
||||
onSubmit={(fields) => {
|
||||
setEditOrder(null);
|
||||
orderEdit.edit({ price: fields.limitPrice, size: fields.size });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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: OrderEventFieldsFragment | 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: OrderEventFieldsFragment | 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;
|
||||
};
|
||||
|
@ -155,10 +155,11 @@ export const useOrderListData = ({
|
||||
).length;
|
||||
}
|
||||
}
|
||||
dataRef.current = filterOrders(data, variables);
|
||||
const filteredData = filterOrders(data, variables);
|
||||
dataRef.current = filteredData;
|
||||
const avoidRerender = !!(
|
||||
(dataRef.current?.length && data?.length) ||
|
||||
(!dataRef.current?.length && !data?.length)
|
||||
(dataRef.current?.length && filteredData?.length) ||
|
||||
(!dataRef.current?.length && !filteredData?.length)
|
||||
);
|
||||
gridRef.current?.api?.refreshInfiniteCache();
|
||||
return avoidRerender;
|
||||
|
@ -16,11 +16,9 @@ import {
|
||||
} from '../mocks/generate-orders';
|
||||
|
||||
const defaultProps: OrderListTableProps = {
|
||||
hasActiveOrder: true,
|
||||
rowData: [],
|
||||
setEditOrder: jest.fn(),
|
||||
cancel: jest.fn(),
|
||||
cancelAll: jest.fn(),
|
||||
};
|
||||
|
||||
const generateJsx = (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { OrderList, OrderListTable } from './order-list';
|
||||
import { OrderListTable } from './order-list';
|
||||
import { useState } from 'react';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
@ -8,8 +8,8 @@ import { OrderEditDialog } from './order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
|
||||
export default {
|
||||
component: OrderList,
|
||||
title: 'OrderList',
|
||||
component: OrderListTable,
|
||||
title: 'OrderListTable',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => {
|
||||
@ -17,10 +17,8 @@ const Template: Story = (args) => {
|
||||
return (
|
||||
<div style={{ height: 1000 }}>
|
||||
<OrderListTable
|
||||
hasActiveOrder
|
||||
rowData={args.data}
|
||||
cancel={cancel}
|
||||
cancelAll={cancel}
|
||||
setEditOrder={() => {
|
||||
return;
|
||||
}}
|
||||
@ -47,10 +45,8 @@ const Template2: Story = (args) => {
|
||||
<>
|
||||
<div style={{ height: 1000 }}>
|
||||
<OrderListTable
|
||||
hasActiveOrder
|
||||
rowData={args.data}
|
||||
cancel={cancel}
|
||||
cancelAll={cancel}
|
||||
setEditOrder={setEditOrder}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
@ -6,28 +5,19 @@ import {
|
||||
negativeClassNames,
|
||||
positiveClassNames,
|
||||
t,
|
||||
truncateByChars,
|
||||
SetFilter,
|
||||
DateRangeFilter,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
Button,
|
||||
Intent,
|
||||
ButtonLink,
|
||||
Link,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
|
||||
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
|
||||
import { useOrderEdit } from '../../order-hooks/use-order-edit';
|
||||
import { OrderFeedback } from '../order-feedback';
|
||||
import { OrderEditDialog } from './order-edit-dialog';
|
||||
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
@ -35,124 +25,16 @@ import type {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import type { OrderEventFieldsFragment } from '../../order-hooks';
|
||||
|
||||
type OrderListProps = TypedDataAgGrid<Order> & { marketId?: string };
|
||||
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
||||
(props, ref) => {
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
const orderCancel = useOrderCancel();
|
||||
const orderEdit = useOrderEdit(editOrder);
|
||||
const hasActiveOrder = useHasActiveOrder(props.marketId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrderListTable
|
||||
{...props}
|
||||
hasActiveOrder={hasActiveOrder}
|
||||
cancelAll={() => {
|
||||
orderCancel.cancel({
|
||||
marketId: props.marketId,
|
||||
});
|
||||
}}
|
||||
cancel={(order: Order) => {
|
||||
if (!order.market) return;
|
||||
orderCancel.cancel({
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
});
|
||||
}}
|
||||
ref={ref}
|
||||
setEditOrder={setEditOrder}
|
||||
/>
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel)}
|
||||
intent={getCancelDialogIntent(orderCancel)}
|
||||
content={{
|
||||
Complete: orderCancel.cancelledOrder ? (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
) : (
|
||||
<TransactionComplete {...orderCancel} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) setEditOrder(null);
|
||||
}}
|
||||
order={editOrder}
|
||||
onSubmit={(fields) => {
|
||||
setEditOrder(null);
|
||||
orderEdit.edit({ price: fields.limitPrice, size: fields.size });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export type OrderListTableProps = OrderListProps & {
|
||||
cancel: (order: Order) => void;
|
||||
cancelAll: () => void;
|
||||
hasActiveOrder: boolean;
|
||||
setEditOrder: (order: Order) => void;
|
||||
};
|
||||
|
||||
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
({ cancel, cancelAll, setEditOrder, hasActiveOrder, ...props }, ref) => {
|
||||
({ cancel, setEditOrder, ...props }, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
ref={ref}
|
||||
@ -164,8 +46,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
getRowId={({ data }) => data.id}
|
||||
rowHeight={34}
|
||||
pinnedBottomRowData={[{}]}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
@ -202,9 +82,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'size'>) => {
|
||||
if (node?.rowPinned) {
|
||||
return '';
|
||||
}
|
||||
if (!data?.market || !isNumeric(value)) {
|
||||
return '-';
|
||||
}
|
||||
@ -230,9 +107,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'type'>) => {
|
||||
if (node?.rowPinned) {
|
||||
return '';
|
||||
}
|
||||
if (!value) return '-';
|
||||
if (order?.peggedOrder) return t('Pegged');
|
||||
if (order?.liquidityProvision) return t('Liquidity provision');
|
||||
@ -279,9 +153,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'remaining'>) => {
|
||||
if (node?.rowPinned) {
|
||||
return '';
|
||||
}
|
||||
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
|
||||
return '-';
|
||||
}
|
||||
@ -304,9 +175,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'price'>) => {
|
||||
if (node?.rowPinned) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
!data?.market ||
|
||||
data.type === Schema.OrderType.TYPE_MARKET ||
|
||||
@ -355,9 +223,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'updatedAt'>) => {
|
||||
if (node?.rowPinned) {
|
||||
return '';
|
||||
}
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||
}}
|
||||
/>
|
||||
@ -366,44 +231,22 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
headerName=""
|
||||
field="status"
|
||||
minWidth={150}
|
||||
type="rightAligned"
|
||||
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
|
||||
if (node?.rowPinned) {
|
||||
return (
|
||||
hasActiveOrder && (
|
||||
<div className="flex gap-2 items-center h-full justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
data-testid="cancelAll"
|
||||
onClick={() => cancelAll()}
|
||||
>
|
||||
{t('Cancel all')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (isOrderAmendable(data)) {
|
||||
return data ? (
|
||||
<div className="flex gap-2 items-center h-full justify-end">
|
||||
<Button
|
||||
data-testid="edit"
|
||||
onClick={() => setEditOrder(data)}
|
||||
size="xs"
|
||||
>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
data-testid="cancel"
|
||||
onClick={() => cancel(data)}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return data && isOrderAmendable(data) ? (
|
||||
<>
|
||||
<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>
|
||||
@ -436,76 +279,3 @@ export const isOrderAmendable = (order: Order | undefined) => {
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
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: OrderEventFieldsFragment | 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: OrderEventFieldsFragment | 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;
|
||||
};
|
||||
|
@ -24,20 +24,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
} = useClosePosition();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No positions')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
>
|
||||
<PositionsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onClose={(position) => submit(position)}
|
||||
<div className="h-full relative">
|
||||
<PositionsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onClose={(position) => submit(position)}
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No positions')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
</div>
|
||||
<Dialog
|
||||
intent={getDialogIntent(transactionResult)}
|
||||
icon={getDialogIcon(transactionResult)}
|
||||
@ -55,7 +57,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -62,12 +62,11 @@ it('Render correct columns', async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Splits market name', async () => {
|
||||
it('renders market name', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
expect(screen.getByText('ETH/BTC')).toBeTruthy();
|
||||
expect(screen.getByText('31 july 2022')).toBeTruthy();
|
||||
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Does not fail if the market name does not match the split pattern', async () => {
|
||||
|
@ -25,30 +25,15 @@ import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ButtonLink, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { getRowId } from './use-positions-data';
|
||||
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface Props extends TypedDataAgGrid<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export interface MarketNameCellProps {
|
||||
valueFormatted?: [string, string];
|
||||
}
|
||||
|
||||
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
|
||||
if (valueFormatted && valueFormatted[1]) {
|
||||
return (
|
||||
<div className="leading-tight">
|
||||
<div>{valueFormatted[0]}</div>
|
||||
<div>{valueFormatted[1]}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (valueFormatted && valueFormatted[0]) || undefined;
|
||||
};
|
||||
|
||||
export interface AmountCellProps {
|
||||
valueFormatted?: Pick<
|
||||
Position,
|
||||
@ -80,24 +65,6 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
|
||||
|
||||
AmountCell.displayName = 'AmountCell';
|
||||
|
||||
const ButtonCell = ({
|
||||
onClick,
|
||||
data,
|
||||
}: {
|
||||
onClick: (position: Position) => void;
|
||||
data: Position;
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
data-testid="close-position"
|
||||
onClick={() => onClick(data)}
|
||||
size="xs"
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
({ onClose, ...props }, ref) => {
|
||||
return (
|
||||
@ -105,7 +72,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No positions')}
|
||||
getRowId={getRowId}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
@ -119,24 +85,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketName"
|
||||
cellRenderer={MarketNameCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'marketName'>) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
// split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
|
||||
const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
|
||||
if (matches && matches[1] && matches[3]) {
|
||||
return [matches[1].trim(), matches[3].trim()];
|
||||
}
|
||||
return [value];
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn headerName={t('Market')} field="marketName" />
|
||||
<AgGridColumn
|
||||
headerName={t('Notional')}
|
||||
headerTooltip={t('Mark price x open volume.')}
|
||||
@ -413,12 +362,15 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
/>
|
||||
{onClose ? (
|
||||
<AgGridColumn
|
||||
cellRendererSelector={(): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: ButtonCell,
|
||||
};
|
||||
}}
|
||||
cellRendererParams={{ onClick: onClose }}
|
||||
type="rightAligned"
|
||||
cellRenderer={({ data }: VegaICellRendererParams<Position>) => (
|
||||
<ButtonLink
|
||||
data-testid="close-position"
|
||||
onClick={() => data && onClose(data)}
|
||||
>
|
||||
{t('Close')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</AgGrid>
|
||||
|
@ -20,13 +20,17 @@ const agGridDarkVariables = `
|
||||
|
||||
.ag-theme-balham-dark .ag-row {
|
||||
border-width: 1px 0;
|
||||
border-bottom: solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ag-theme-balham-dark .ag-react-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
`;
|
||||
|
||||
export const AgGrid = ({
|
||||
|
@ -20,13 +20,17 @@ const agGridLightVariables = `
|
||||
|
||||
.ag-theme-balham .ag-row {
|
||||
border-width: 1px 0;
|
||||
border-bottom: solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ag-theme-balham .ag-react-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
`;
|
||||
|
||||
export const AgGrid = ({
|
||||
|
@ -4,7 +4,6 @@ export * from './lib/withdraw-form';
|
||||
export * from './lib/withdraw-form-container';
|
||||
export * from './lib/withdraw-manager';
|
||||
export * from './lib/withdrawals-table';
|
||||
export * from './lib/pending-withdrawals-table';
|
||||
export * from './lib/withdrawal-feedback';
|
||||
export * from './lib/use-complete-withdraw';
|
||||
export * from './lib/use-create-withdraw';
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { generateWithdrawal } from './test-helpers';
|
||||
import { CompleteCell } from './pending-withdrawals-table';
|
||||
import { PendingWithdrawalsTable } from './pending-withdrawals-table';
|
||||
import { getTimeFormat } from '@vegaprotocol/react-helpers';
|
||||
import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
|
||||
|
||||
jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({ provider: undefined }),
|
||||
}));
|
||||
|
||||
const generateTable = (props: TypedDataAgGrid<WithdrawalFieldsFragment>) => (
|
||||
<MockedProvider>
|
||||
<PendingWithdrawalsTable {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
describe('PendingWithdrawalsTable', () => {
|
||||
it('displays correct columns', async () => {
|
||||
const withdrawal = generateWithdrawal();
|
||||
await act(async () => {
|
||||
render(generateTable({ rowData: [withdrawal] }));
|
||||
});
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(5);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||
'Asset',
|
||||
'Amount',
|
||||
'Recipient',
|
||||
'Created',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
it('displays given withdrawals', async () => {
|
||||
const withdrawal = generateWithdrawal();
|
||||
await act(async () => {
|
||||
render(generateTable({ rowData: [withdrawal] }));
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'asset-symbol',
|
||||
'1.00',
|
||||
'123456…123456',
|
||||
getTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||
'Complete withdrawal',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CompleteCell', () => {
|
||||
const mockComplete = jest.fn();
|
||||
const data = generateWithdrawal();
|
||||
it('opens the dialog', () => {
|
||||
render(<CompleteCell complete={mockComplete} data={data} />);
|
||||
fireEvent.click(
|
||||
screen.getByText('Complete withdrawal', { selector: 'button' })
|
||||
);
|
||||
expect(mockComplete).toBeCalled();
|
||||
});
|
||||
});
|
@ -1,177 +0,0 @@
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import {
|
||||
getDateTimeFormat,
|
||||
t,
|
||||
truncateByChars,
|
||||
addDecimalsFormatNumber,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
TypedDataAgGrid,
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Link,
|
||||
AgGridDynamic as AgGrid,
|
||||
Intent,
|
||||
Loader,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
|
||||
import type { VerifyState } from './use-verify-withdrawal';
|
||||
import { ApprovalStatus } from './use-verify-withdrawal';
|
||||
|
||||
export const PendingWithdrawalsTable = (
|
||||
props: TypedDataAgGrid<WithdrawalFieldsFragment>
|
||||
) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const createWithdrawApproval = useEthWithdrawApprovalsStore(
|
||||
(store) => store.create
|
||||
);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
overlayNoRowsTemplate={t('No withdrawals')}
|
||||
defaultColDef={{ flex: 1, resizable: true }}
|
||||
style={{ width: '100%' }}
|
||||
components={{ CompleteCell }}
|
||||
suppressCellFocus={true}
|
||||
domLayout="autoHeight"
|
||||
rowHeight={30}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||
<AgGridColumn
|
||||
headerName={t('Amount')}
|
||||
field="amount"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'amount'>) => {
|
||||
return isNumeric(value) && data?.asset
|
||||
? addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
: null;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Recipient')}
|
||||
field="details.receiverAddress"
|
||||
cellRenderer={({
|
||||
ethUrl,
|
||||
value,
|
||||
valueFormatted,
|
||||
}: VegaICellRendererParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'details.receiverAddress'
|
||||
> & {
|
||||
ethUrl: string;
|
||||
}) => (
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ethUrl}/address/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{valueFormatted}
|
||||
</Link>
|
||||
)}
|
||||
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'details.receiverAddress'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
return truncateByChars(value);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Created')}
|
||||
field="createdTimestamp"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'createdTimestamp'
|
||||
>) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
field="status"
|
||||
flex={2}
|
||||
cellRendererParams={{
|
||||
complete: (withdrawal: WithdrawalFieldsFragment) => {
|
||||
createWithdrawApproval(withdrawal);
|
||||
},
|
||||
}}
|
||||
cellRenderer="CompleteCell"
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export type CompleteCellProps = {
|
||||
data: WithdrawalFieldsFragment;
|
||||
complete: (withdrawal: WithdrawalFieldsFragment) => void;
|
||||
};
|
||||
export const CompleteCell = ({ data, complete }: CompleteCellProps) => (
|
||||
<Button
|
||||
data-testid="complete-withdrawal"
|
||||
size="xs"
|
||||
onClick={() => complete(data)}
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
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>{t('Something went wrong')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Pending) {
|
||||
return <p>{t('Verifying...')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
|
||||
const formattedTime = getDateTimeFormat().format(
|
||||
new Date(state.completeTimestamp)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t("The amount you're withdrawing has triggered a time delay")}
|
||||
</p>
|
||||
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
@ -1,8 +1,7 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
useWithdrawalsQuery,
|
||||
WithdrawalEventDocument,
|
||||
@ -27,7 +26,7 @@ export const useWithdrawals = () => {
|
||||
useEffect(() => {
|
||||
if (!pubKey) return;
|
||||
|
||||
const unsub = subscribeToMore<
|
||||
const unsubscribe = subscribeToMore<
|
||||
WithdrawalEventSubscription,
|
||||
WithdrawalEventSubscriptionVariables
|
||||
>({
|
||||
@ -37,49 +36,23 @@ export const useWithdrawals = () => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [pubKey, subscribeToMore]);
|
||||
|
||||
const withdrawals = useMemo(() => {
|
||||
if (!data?.party?.withdrawalsConnection?.edges) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
removePaginationWrapper(data.party.withdrawalsConnection.edges),
|
||||
'createdTimestamp',
|
||||
'desc'
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
/**
|
||||
* withdrawals that have to be completed by a user.
|
||||
*/
|
||||
const pending = useMemo(() => {
|
||||
return withdrawals.filter((w) => !w.txHash);
|
||||
}, [withdrawals]);
|
||||
|
||||
/**
|
||||
* withdrawals that are completed or being completed
|
||||
*/
|
||||
const completed = useMemo(() => {
|
||||
return withdrawals
|
||||
.filter((w) => w.txHash)
|
||||
.sort((a, b) =>
|
||||
(b.withdrawnTimestamp || b.createdTimestamp).localeCompare(
|
||||
a.withdrawnTimestamp || a.createdTimestamp
|
||||
)
|
||||
);
|
||||
}, [withdrawals]);
|
||||
|
||||
return {
|
||||
data,
|
||||
data: removePaginationWrapper(
|
||||
data?.party?.withdrawalsConnection?.edges ?? []
|
||||
).sort((a, b) => {
|
||||
if (!b.txHash !== !a.txHash) {
|
||||
return b.txHash ? -1 : 1;
|
||||
}
|
||||
return (
|
||||
b.txHash ? b.withdrawnTimestamp : b.createdTimestamp
|
||||
).localeCompare(a.txHash ? a.withdrawnTimestamp : a.createdTimestamp);
|
||||
}),
|
||||
loading,
|
||||
error,
|
||||
withdrawals,
|
||||
pending,
|
||||
completed,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -26,11 +26,12 @@ describe('renders the correct columns', () => {
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(6);
|
||||
expect(headers).toHaveLength(7);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||
'Asset',
|
||||
'Amount',
|
||||
'Recipient',
|
||||
'Created',
|
||||
'Completed',
|
||||
'Status',
|
||||
'Transaction',
|
||||
@ -41,9 +42,10 @@ describe('renders the correct columns', () => {
|
||||
'asset-symbol',
|
||||
'1.00',
|
||||
'123456…123456',
|
||||
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)),
|
||||
'-',
|
||||
'Pending',
|
||||
'-',
|
||||
'Complete withdrawal',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
@ -66,6 +68,7 @@ describe('renders the correct columns', () => {
|
||||
'asset-symbol',
|
||||
'1.00',
|
||||
'123456…123456',
|
||||
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)),
|
||||
getTimeFormat().format(new Date(withdrawal.withdrawnTimestamp as string)),
|
||||
'Completed',
|
||||
'0x1234…121314',
|
||||
|
@ -11,25 +11,42 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Link,
|
||||
ButtonLink,
|
||||
AgGridDynamic as AgGrid,
|
||||
Intent,
|
||||
Icon,
|
||||
Loader,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { VerifyState } from './use-verify-withdrawal';
|
||||
import { ApprovalStatus } from './use-verify-withdrawal';
|
||||
|
||||
export const WithdrawalsTable = (
|
||||
props: TypedDataAgGrid<WithdrawalFieldsFragment>
|
||||
) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const createWithdrawApproval = useEthWithdrawApprovalsStore(
|
||||
(store) => store.create
|
||||
);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
overlayNoRowsTemplate={t('No withdrawals')}
|
||||
defaultColDef={{ flex: 1, resizable: true }}
|
||||
style={{ width: '100%' }}
|
||||
components={{ RecipientCell, StatusCell }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
components={{
|
||||
RecipientCell,
|
||||
StatusCell,
|
||||
EtherscanLinkCell,
|
||||
CompleteCell,
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
domLayout="autoHeight"
|
||||
rowHeight={30}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||
@ -60,19 +77,25 @@ export const WithdrawalsTable = (
|
||||
return truncateByChars(value);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Created')}
|
||||
field="createdTimestamp"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'createdTimestamp'
|
||||
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Completed')}
|
||||
field="withdrawnTimestamp"
|
||||
valueFormatter={({
|
||||
data,
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'withdrawnTimestamp'
|
||||
>) => {
|
||||
const ts = data?.withdrawnTimestamp;
|
||||
if (!ts) return '-';
|
||||
return getDateTimeFormat().format(new Date(ts));
|
||||
}}
|
||||
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
@ -82,26 +105,57 @@ export const WithdrawalsTable = (
|
||||
<AgGridColumn
|
||||
headerName={t('Transaction')}
|
||||
field="txHash"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'>) => {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
);
|
||||
flex={2}
|
||||
cellRendererParams={{
|
||||
complete: (withdrawal: WithdrawalFieldsFragment) => {
|
||||
createWithdrawApproval(withdrawal);
|
||||
},
|
||||
}}
|
||||
cellRendererSelector={({
|
||||
data,
|
||||
}: VegaICellRendererParams<WithdrawalFieldsFragment>) => ({
|
||||
component: data?.txHash ? 'EtherscanLinkCell' : 'CompleteCell',
|
||||
})}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export type CompleteCellProps = {
|
||||
data: WithdrawalFieldsFragment;
|
||||
complete: (withdrawal: WithdrawalFieldsFragment) => void;
|
||||
};
|
||||
export const CompleteCell = ({ data, complete }: CompleteCellProps) =>
|
||||
data.pendingOnForeignChain ? (
|
||||
'-'
|
||||
) : (
|
||||
<ButtonLink
|
||||
data-testid="complete-withdrawal"
|
||||
onClick={() => complete(data)}
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</ButtonLink>
|
||||
);
|
||||
|
||||
export const EtherscanLinkCell = ({
|
||||
value,
|
||||
ethUrl,
|
||||
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'> & {
|
||||
ethUrl: string;
|
||||
}) => {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ethUrl}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusCell = ({ data }: { data: WithdrawalFieldsFragment }) => {
|
||||
if (data.pendingOnForeignChain || !data.txHash) {
|
||||
return <span>{t('Pending')}</span>;
|
||||
@ -139,3 +193,48 @@ const RecipientCell = ({
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
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>{t('Something went wrong')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Pending) {
|
||||
return <p>{t('Verifying...')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
|
||||
const formattedTime = getDateTimeFormat().format(
|
||||
new Date(state.completeTimestamp)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t("The amount you're withdrawing has triggered a time delay")}
|
||||
</p>
|
||||
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user