chore(trading): filtering and sorting for ledger entries (#2944)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
parent
5bde096977
commit
c29087cc96
@ -45,8 +45,9 @@ export const FillsManager = ({
|
|||||||
datasource={{ getRows }}
|
datasource={{ getRows }}
|
||||||
onBodyScrollEnd={onBodyScrollEnd}
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
onBodyScroll={onBodyScroll}
|
onBodyScroll={onBodyScroll}
|
||||||
noRowsOverlayComponent={() => null}
|
|
||||||
onMarketClick={onMarketClick}
|
onMarketClick={onMarketClick}
|
||||||
|
suppressLoadingOverlay
|
||||||
|
suppressNoRowsOverlay
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-0">
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
|
@ -2,10 +2,8 @@ import type { Asset } from '@vegaprotocol/assets';
|
|||||||
import { assetsProvider } from '@vegaprotocol/assets';
|
import { assetsProvider } from '@vegaprotocol/assets';
|
||||||
import type { Market } from '@vegaprotocol/market-list';
|
import type { Market } from '@vegaprotocol/market-list';
|
||||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||||
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
|
||||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
|
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
|
||||||
import {
|
import {
|
||||||
defaultAppend as append,
|
|
||||||
makeDataProvider,
|
makeDataProvider,
|
||||||
makeDerivedDataProvider,
|
makeDerivedDataProvider,
|
||||||
useDataProvider,
|
useDataProvider,
|
||||||
@ -27,13 +25,15 @@ import type {
|
|||||||
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
|
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
|
||||||
|
|
||||||
export type LedgerEntry = LedgerEntryFragment & {
|
export type LedgerEntry = LedgerEntryFragment & {
|
||||||
id: number;
|
|
||||||
asset: Asset | null | undefined;
|
asset: Asset | null | undefined;
|
||||||
marketSender: Market | null | undefined;
|
marketSender: Market | null | undefined;
|
||||||
marketReceiver: Market | null | undefined;
|
marketReceiver: Market | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||||
|
export type AggregatedLedgerEntriesNode = AggregatedLedgerEntriesEdge & {
|
||||||
|
node: LedgerEntry;
|
||||||
|
};
|
||||||
|
|
||||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||||
return responseData?.ledgerEntries?.edges || [];
|
return responseData?.ledgerEntries?.edges || [];
|
||||||
@ -94,33 +94,29 @@ export const update = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPageInfo = (responseData: LedgerEntriesQuery): PageInfo | null =>
|
|
||||||
responseData.ledgerEntries?.pageInfo || null;
|
|
||||||
|
|
||||||
const ledgerEntriesOnlyProvider = makeDataProvider({
|
const ledgerEntriesOnlyProvider = makeDataProvider({
|
||||||
query: LedgerEntriesDocument,
|
query: LedgerEntriesDocument,
|
||||||
getData,
|
getData,
|
||||||
getDelta: getData,
|
getDelta: getData,
|
||||||
update,
|
update,
|
||||||
pagination: {
|
|
||||||
getPageInfo,
|
|
||||||
append,
|
|
||||||
first: 100,
|
|
||||||
},
|
|
||||||
additionalContext: {
|
additionalContext: {
|
||||||
isEnlargedTimeout: true,
|
isEnlargedTimeout: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
||||||
(AggregatedLedgerEntriesEdge | null)[],
|
AggregatedLedgerEntriesNode[],
|
||||||
AggregatedLedgerEntriesEdge[],
|
AggregatedLedgerEntriesNode[],
|
||||||
LedgerEntriesQueryVariables
|
LedgerEntriesQueryVariables
|
||||||
>(
|
>(
|
||||||
[ledgerEntriesOnlyProvider, assetsProvider, marketsProvider],
|
[
|
||||||
|
ledgerEntriesOnlyProvider,
|
||||||
|
(callback, client) => assetsProvider(callback, client),
|
||||||
|
marketsProvider,
|
||||||
|
],
|
||||||
([entries, assets, markets]) => {
|
([entries, assets, markets]) => {
|
||||||
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
||||||
const entry = edge?.node;
|
const entry = edge.node;
|
||||||
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
||||||
const marketSender = markets.find(
|
const marketSender = markets.find(
|
||||||
(market: Market) => market.id === entry.fromAccountMarketId
|
(market: Market) => market.id === entry.fromAccountMarketId
|
||||||
@ -148,21 +144,22 @@ export const useLedgerEntriesDataProvider = ({
|
|||||||
filter,
|
filter,
|
||||||
gridRef,
|
gridRef,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const dataRef = useRef<(AggregatedLedgerEntriesEdge | null)[] | null>(null);
|
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
|
||||||
const totalCountRef = useRef<number>();
|
const totalCountRef = useRef<number>();
|
||||||
|
|
||||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||||
() => ({
|
() => ({
|
||||||
partyId,
|
partyId,
|
||||||
dateRange: filter?.vegaTime?.value,
|
dateRange: filter?.vegaTime?.value,
|
||||||
fromAccountType: filter?.fromAccountType?.value ?? null,
|
pagination: {
|
||||||
toAccountType: filter?.toAccountType?.value ?? null,
|
first: 5000,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[partyId, filter]
|
[partyId, filter?.vegaTime?.value]
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
({ data }: { data: (AggregatedLedgerEntriesEdge | null)[] | null }) => {
|
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
|
||||||
return updateGridData(dataRef, data, gridRef);
|
return updateGridData(dataRef, data, gridRef);
|
||||||
},
|
},
|
||||||
[gridRef]
|
[gridRef]
|
||||||
@ -173,7 +170,7 @@ export const useLedgerEntriesDataProvider = ({
|
|||||||
data,
|
data,
|
||||||
totalCount,
|
totalCount,
|
||||||
}: {
|
}: {
|
||||||
data: (AggregatedLedgerEntriesEdge | null)[] | null;
|
data: AggregatedLedgerEntriesEdge[] | null;
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}) => {
|
}) => {
|
||||||
totalCountRef.current = totalCount;
|
totalCountRef.current = totalCount;
|
||||||
|
@ -3,7 +3,9 @@ import type * as Schema from '@vegaprotocol/types';
|
|||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { subDays, formatRFC3339 } from 'date-fns';
|
||||||
|
import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider';
|
||||||
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
||||||
import { LedgerTable } from './ledger-table';
|
import { LedgerTable } from './ledger-table';
|
||||||
import type * as Types from '@vegaprotocol/types';
|
import type * as Types from '@vegaprotocol/types';
|
||||||
@ -15,43 +17,43 @@ export interface Filter {
|
|||||||
fromAccountType?: { value: Types.AccountType[] };
|
fromAccountType?: { value: Types.AccountType[] };
|
||||||
toAccountType?: { value: Types.AccountType[] };
|
toAccountType?: { value: Types.AccountType[] };
|
||||||
}
|
}
|
||||||
|
const defaultFilter = {
|
||||||
type LedgerManagerProps = { partyId: string };
|
vegaTime: {
|
||||||
export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const [filter, setFilter] = useState<Filter | undefined>();
|
const [filter, setFilter] = useState<Filter>(defaultFilter);
|
||||||
|
const [dataCount, setDataCount] = useState(0);
|
||||||
|
|
||||||
const { data, error, loading, getRows, reload } =
|
const { data, error, loading, reload } = useLedgerEntriesDataProvider({
|
||||||
useLedgerEntriesDataProvider({
|
partyId,
|
||||||
partyId,
|
filter,
|
||||||
filter,
|
gridRef,
|
||||||
gridRef,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const onFilterChanged = useCallback(
|
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||||
(event: FilterChangedEvent) => {
|
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
|
||||||
const updatedFilter = event.api.getFilterModel();
|
setFilter(updatedFilter);
|
||||||
if (Object.keys(updatedFilter).length) {
|
}, []);
|
||||||
setFilter(updatedFilter);
|
const extractNodesDecorator = useCallback(
|
||||||
} else if (filter) {
|
(data: AggregatedLedgerEntriesNode[] | null, loading: boolean) =>
|
||||||
setFilter(undefined);
|
data && !loading ? data.map((item) => item.node) : null,
|
||||||
}
|
|
||||||
},
|
|
||||||
[filter]
|
|
||||||
);
|
|
||||||
const getRowId = useCallback(
|
|
||||||
({ data }: { data: Types.AggregatedLedgerEntry }) =>
|
|
||||||
`${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`,
|
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const extractedData = extractNodesDecorator(data, loading);
|
||||||
|
useEffect(() => {
|
||||||
|
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||||
|
}, [extractedData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<LedgerTable
|
<LedgerTable
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
rowModelType="infinite"
|
rowData={extractedData}
|
||||||
datasource={{ getRows }}
|
|
||||||
onFilterChanged={onFilterChanged}
|
onFilterChanged={onFilterChanged}
|
||||||
getRowId={getRowId}
|
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-0">
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
@ -59,7 +61,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
|||||||
error={error}
|
error={error}
|
||||||
data={data}
|
data={data}
|
||||||
noDataMessage={t('No entries')}
|
noDataMessage={t('No entries')}
|
||||||
noDataCondition={(data) => !(data && data.length)}
|
noDataCondition={() => !dataCount}
|
||||||
reload={reload}
|
reload={reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import type { LedgerEntry } from './ledger-entries-data-provider';
|
import type { LedgerEntry } from './ledger-entries-data-provider';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
import { formatRFC3339, subDays } from 'date-fns';
|
||||||
|
|
||||||
export const TransferTooltipCellComponent = ({
|
export const TransferTooltipCellComponent = ({
|
||||||
value,
|
value,
|
||||||
@ -35,6 +36,11 @@ export const TransferTooltipCellComponent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
|
||||||
|
const dateRangeFilterParams = {
|
||||||
|
maxNextDays: 0,
|
||||||
|
defaultRangeFilter,
|
||||||
|
};
|
||||||
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
||||||
|
|
||||||
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||||
@ -42,17 +48,20 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
|||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
overlayNoRowsTemplate={t('No entries')}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
getRowId={({ data }) => data.id}
|
|
||||||
tooltipShowDelay={500}
|
tooltipShowDelay={500}
|
||||||
defaultColDef={{
|
defaultColDef={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
tooltipComponent: TransferTooltipCellComponent,
|
tooltipComponent: TransferTooltipCellComponent,
|
||||||
filterParams: { buttons: ['reset'] },
|
filterParams: {
|
||||||
|
...dateRangeFilterParams,
|
||||||
|
buttons: ['reset'],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
suppressLoadingOverlay
|
||||||
|
suppressNoRowsOverlay
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
@ -201,6 +210,7 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
|||||||
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
|
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
|
||||||
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
|
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
|
||||||
}
|
}
|
||||||
|
filterParams={dateRangeFilterParams}
|
||||||
filter={DateRangeFilter}
|
filter={DateRangeFilter}
|
||||||
/>
|
/>
|
||||||
</AgGrid>
|
</AgGrid>
|
||||||
|
@ -152,6 +152,8 @@ export const OrderListManager = ({
|
|||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
hasActiveOrder={hasActiveOrder}
|
hasActiveOrder={hasActiveOrder}
|
||||||
blockLoadDebounceMillis={100}
|
blockLoadDebounceMillis={100}
|
||||||
|
suppressLoadingOverlay
|
||||||
|
suppressNoRowsOverlay
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-0">
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
process.env.TZ = 'UTC';
|
||||||
export default {
|
export default {
|
||||||
displayName: 'react-helpers',
|
displayName: 'react-helpers',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
|
29
libs/react-helpers/src/lib/grid/date-range-filter.spec.tsx
Normal file
29
libs/react-helpers/src/lib/grid/date-range-filter.spec.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import type { DateRangeFilterProps } from './date-range-filter';
|
||||||
|
import { DateRangeFilter } from './date-range-filter';
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
filterChangedCallback: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('DateRangeFilter', () => {
|
||||||
|
it('should be properly rendered', async () => {
|
||||||
|
const defaultRangeFilter = {
|
||||||
|
start: '2023-02-14T13:53:01+01:00',
|
||||||
|
end: '2023-02-21T13:53:01+01:00',
|
||||||
|
};
|
||||||
|
const displayStartValue = '2023-02-14T12:53:01.000';
|
||||||
|
const displayEndValue = '2023-02-21T12:53:01.000';
|
||||||
|
render(
|
||||||
|
<DateRangeFilter
|
||||||
|
{...(commonProps as unknown as DateRangeFilterProps)}
|
||||||
|
defaultRangeFilter={defaultRangeFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('Start')).toHaveValue(displayStartValue);
|
||||||
|
expect(screen.getByLabelText('End')).toHaveValue(displayEndValue);
|
||||||
|
|
||||||
|
expect(commonProps.filterChangedCallback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -1,113 +1,259 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import type * as Schema from '@vegaprotocol/types';
|
import type * as Schema from '@vegaprotocol/types';
|
||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||||
import { formatForInput } from '../format/date';
|
import {
|
||||||
|
isBefore,
|
||||||
|
subDays,
|
||||||
|
addDays,
|
||||||
|
differenceInDays,
|
||||||
|
formatRFC3339,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
isValid,
|
||||||
|
} from 'date-fns';
|
||||||
import { t } from '../i18n';
|
import { t } from '../i18n';
|
||||||
|
import { formatForInput } from '../format/date';
|
||||||
|
|
||||||
const defaultFilterValue: Schema.DateRange = {};
|
const defaultFilterValue: Schema.DateRange = {};
|
||||||
|
export interface DateRangeFilterProps extends IFilterParams {
|
||||||
|
defaultRangeFilter?: Schema.DateRange;
|
||||||
|
maxSubDays?: number;
|
||||||
|
maxNextDays?: number;
|
||||||
|
maxDaysRange?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => {
|
export const DateRangeFilter = forwardRef(
|
||||||
const [value, setValue] = useState<Schema.DateRange>(defaultFilterValue);
|
(props: DateRangeFilterProps, ref) => {
|
||||||
|
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
|
||||||
|
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
|
||||||
|
const minStartDate =
|
||||||
|
props?.maxSubDays !== undefined
|
||||||
|
? formatForInput(subDays(Date.now(), props.maxSubDays))
|
||||||
|
: '';
|
||||||
|
const maxStartDate =
|
||||||
|
props?.maxNextDays !== undefined
|
||||||
|
? formatForInput(addDays(Date.now(), props.maxNextDays))
|
||||||
|
: '';
|
||||||
|
const minEndDate =
|
||||||
|
value.start && props?.maxDaysRange !== undefined
|
||||||
|
? formatForInput(new Date(value.start))
|
||||||
|
: minStartDate || value.start
|
||||||
|
? formatForInput(new Date(value.start))
|
||||||
|
: '';
|
||||||
|
const endDateCandidates = [];
|
||||||
|
if (props.maxNextDays !== undefined) {
|
||||||
|
endDateCandidates.push(addDays(new Date(), props.maxNextDays));
|
||||||
|
}
|
||||||
|
if (props.maxDaysRange !== undefined && value.start) {
|
||||||
|
endDateCandidates.push(
|
||||||
|
addDays(new Date(value.start), props.maxDaysRange)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const maxEndDate = endDateCandidates.length
|
||||||
|
? formatForInput(min(endDateCandidates))
|
||||||
|
: maxStartDate;
|
||||||
|
return [minStartDate, maxStartDate, minEndDate, maxEndDate];
|
||||||
|
}, [props.maxSubDays, props.maxDaysRange, props.maxNextDays, value.start]);
|
||||||
|
// expose AG Grid Filter Lifecycle callbacks
|
||||||
|
useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
doesFilterPass(params: IDoesFilterPassParams) {
|
||||||
|
const { api, colDef, column, columnApi, context } = props;
|
||||||
|
const { node } = params;
|
||||||
|
const rowValue = props.valueGetter({
|
||||||
|
api,
|
||||||
|
colDef,
|
||||||
|
column,
|
||||||
|
columnApi,
|
||||||
|
context,
|
||||||
|
data: node.data,
|
||||||
|
getValue: (field) => node.data[field],
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
value.start &&
|
||||||
|
rowValue &&
|
||||||
|
new Date(rowValue) <= new Date(value.start)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
value.end &&
|
||||||
|
rowValue &&
|
||||||
|
new Date(rowValue) >= new Date(value.end)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
// expose AG Grid Filter Lifecycle callbacks
|
isFilterActive() {
|
||||||
useImperativeHandle(ref, () => {
|
return value.start || value.end;
|
||||||
return {
|
},
|
||||||
doesFilterPass(params: IDoesFilterPassParams) {
|
|
||||||
const { api, colDef, column, columnApi, context } = props;
|
|
||||||
const { node } = params;
|
|
||||||
const rowValue = props.valueGetter({
|
|
||||||
api,
|
|
||||||
colDef,
|
|
||||||
column,
|
|
||||||
columnApi,
|
|
||||||
context,
|
|
||||||
data: node.data,
|
|
||||||
getValue: (field) => node.data[field],
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
value.start &&
|
|
||||||
rowValue &&
|
|
||||||
new Date(rowValue) <= new Date(value.start)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
value.end &&
|
|
||||||
rowValue &&
|
|
||||||
new Date(rowValue) >= new Date(value.end)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
isFilterActive() {
|
getModel() {
|
||||||
return value.start || value.end;
|
if (!this.isFilterActive()) {
|
||||||
},
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getModel() {
|
return { value };
|
||||||
if (!this.isFilterActive()) {
|
},
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value };
|
setModel(model?: { value: Schema.DateRange } | null) {
|
||||||
},
|
setValue(
|
||||||
|
model?.value || props?.defaultRangeFilter || defaultFilterValue
|
||||||
setModel(model?: { value: Schema.DateRange } | null) {
|
);
|
||||||
setValue(model?.value || defaultFilterValue);
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setValue({
|
|
||||||
...value,
|
|
||||||
[event.target.name]:
|
|
||||||
event.target.value &&
|
|
||||||
new Date(event.target.value).toISOString().replace('Z', '000000Z'),
|
|
||||||
});
|
});
|
||||||
};
|
const validate = (
|
||||||
|
name: string,
|
||||||
|
timeValue: Date,
|
||||||
|
update?: Schema.DateRange
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
props.maxSubDays !== undefined &&
|
||||||
|
isBefore(new Date(timeValue), subDays(Date.now(), props.maxSubDays + 1))
|
||||||
|
) {
|
||||||
|
setError(
|
||||||
|
t(
|
||||||
|
'The earliest data that can be queried is %s days ago.',
|
||||||
|
String(props.maxSubDays)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (props?.maxDaysRange !== undefined) {
|
||||||
|
const contrvalue =
|
||||||
|
name === 'start'
|
||||||
|
? update?.end || value.end
|
||||||
|
: update?.start || value.start;
|
||||||
|
if (
|
||||||
|
Math.abs(
|
||||||
|
differenceInDays(new Date(timeValue), new Date(contrvalue))
|
||||||
|
) > props.maxDaysRange
|
||||||
|
) {
|
||||||
|
setError(
|
||||||
|
t(
|
||||||
|
'The maximum time range that can be queried is %s days.',
|
||||||
|
String(props.maxDaysRange)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setError('');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const checkForEndDate = (
|
||||||
props?.filterChangedCallback();
|
endDate: Date | undefined,
|
||||||
}, [value, props]);
|
startDate: Date | undefined
|
||||||
|
) => {
|
||||||
|
const endDateCandidates: Date[] = [];
|
||||||
|
if (props.maxDaysRange !== undefined && isValid(startDate)) {
|
||||||
|
endDateCandidates.push(addDays(startDate as Date, props.maxDaysRange));
|
||||||
|
}
|
||||||
|
if (props.maxNextDays !== undefined) {
|
||||||
|
endDateCandidates.push(addDays(Date.now(), props.maxNextDays));
|
||||||
|
}
|
||||||
|
if (isValid(endDate)) {
|
||||||
|
endDateCandidates.push(endDate as Date);
|
||||||
|
}
|
||||||
|
return endDate && startDate
|
||||||
|
? formatRFC3339(max([startDate, min(endDateCandidates)]))
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value: dateValue, name } = event.target;
|
||||||
|
const date = new Date(dateValue || defaultDates[name as 'start' | 'end']);
|
||||||
|
let update = { [name]: isValid(date) ? formatRFC3339(date) : undefined };
|
||||||
|
const startCheckDate = name === 'start' ? date : new Date(value.start);
|
||||||
|
const endCheckDate =
|
||||||
|
name === 'start'
|
||||||
|
? new Date(value.end)
|
||||||
|
: isValid(date)
|
||||||
|
? date
|
||||||
|
: new Date(maxEndDate);
|
||||||
|
const endDate = isValid(endCheckDate) ? endCheckDate : undefined;
|
||||||
|
const startDate = isValid(startCheckDate) ? startCheckDate : undefined;
|
||||||
|
update = { ...update, end: checkForEndDate(endDate, startDate) };
|
||||||
|
|
||||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
if (validate(name, date, update)) {
|
||||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
setValue((curr) => ({
|
||||||
return (
|
...curr,
|
||||||
<div className="ag-filter-body-wrapper">
|
...update,
|
||||||
<fieldset className="ag-simple-filter-body-wrapper">
|
}));
|
||||||
<label className="block" key="start">
|
}
|
||||||
<span className="block">{t('Start')}</span>
|
};
|
||||||
<input
|
useEffect(() => {
|
||||||
type="datetime-local"
|
props?.filterChangedCallback();
|
||||||
name="start"
|
}, [value, props]);
|
||||||
value={start}
|
|
||||||
onChange={onChange}
|
const notification = useMemo(() => {
|
||||||
/>
|
const not = error ? (
|
||||||
</label>
|
<div
|
||||||
<label className="block" key="end">
|
className="text-sm flex items-center first-letter:uppercase mt-2 border-danger text-danger"
|
||||||
<span className="block">{t('End')}</span>
|
role="alert"
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
name="end"
|
|
||||||
value={end}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<div className="ag-filter-apply-panel">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="ag-standard-button ag-filter-apply-panel-button"
|
|
||||||
onClick={() => setValue(defaultFilterValue)}
|
|
||||||
>
|
>
|
||||||
Reset
|
{error}
|
||||||
</button>
|
</div>
|
||||||
|
) : null;
|
||||||
|
return (
|
||||||
|
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||||
|
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||||
|
return (
|
||||||
|
<div className="ag-filter-body-wrapper inline-block min-w-fit">
|
||||||
|
{notification}
|
||||||
|
<div className="ag-filter-apply-panel">
|
||||||
|
<fieldset className="ag-simple-filter-body-wrapper">
|
||||||
|
<label className="block" key="start">
|
||||||
|
<span className="block mb-1">{t('Start')}</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
name="start"
|
||||||
|
value={start || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
min={minStartDate}
|
||||||
|
max={maxStartDate}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset className="ag-simple-filter-body-wrapper">
|
||||||
|
<label className="block" key="end">
|
||||||
|
<span className="block mb-1">{t('End')}</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
name="end"
|
||||||
|
value={end || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
min={minEndDate}
|
||||||
|
max={maxEndDate}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div className="ag-filter-apply-panel">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ag-standard-button ag-filter-apply-panel-button"
|
||||||
|
onClick={() => {
|
||||||
|
setError('');
|
||||||
|
setValue(defaultDates);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Reset')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
});
|
);
|
||||||
|
@ -6,6 +6,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||||
|
import { t } from '../i18n';
|
||||||
|
|
||||||
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||||
const [value, setValue] = useState<string[]>([]);
|
const [value, setValue] = useState<string[]>([]);
|
||||||
@ -16,18 +17,19 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
|||||||
doesFilterPass(params: IDoesFilterPassParams) {
|
doesFilterPass(params: IDoesFilterPassParams) {
|
||||||
const { api, colDef, column, columnApi, context } = props;
|
const { api, colDef, column, columnApi, context } = props;
|
||||||
const { node } = params;
|
const { node } = params;
|
||||||
return (
|
const getValue = props.valueGetter({
|
||||||
props.valueGetter({
|
api,
|
||||||
api,
|
colDef,
|
||||||
colDef,
|
column,
|
||||||
column,
|
columnApi,
|
||||||
columnApi,
|
context,
|
||||||
context,
|
data: node.data,
|
||||||
data: node.data,
|
getValue: (field) => node.data[field],
|
||||||
getValue: (field) => node.data[field],
|
node,
|
||||||
node,
|
});
|
||||||
}) === value
|
return Array.isArray(value)
|
||||||
);
|
? value.includes(getValue)
|
||||||
|
: getValue === value;
|
||||||
},
|
},
|
||||||
|
|
||||||
isFilterActive() {
|
isFilterActive() {
|
||||||
@ -83,7 +85,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
|||||||
className="ag-standard-button ag-filter-apply-panel-button"
|
className="ag-standard-button ag-filter-apply-panel-button"
|
||||||
onClick={() => setValue([])}
|
onClick={() => setValue([])}
|
||||||
>
|
>
|
||||||
Reset
|
{t('Reset')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user