diff --git a/icons/columns.svg b/icons/columns.svg
new file mode 100644
index 0000000000..d4bfc97368
--- /dev/null
+++ b/icons/columns.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/api/resources.ts b/lib/api/resources.ts
index f5776d8d72..952688ff6f 100644
--- a/lib/api/resources.ts
+++ b/lib/api/resources.ts
@@ -41,6 +41,7 @@ import type {
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
+import type { AdvancedFilterParams, AdvancedFilterResponse, AdvancedFilterMethodsResponse } from 'types/api/advancedFilter';
import type {
ArbitrumL2MessagesResponse,
ArbitrumL2MessagesItem,
@@ -50,7 +51,6 @@ import type {
ArbitrumL2BatchBlocks,
ArbitrumL2TxnBatchesItem,
} from 'types/api/arbitrumL2';
-import type { AdvancedFilterParams, AdvancedFilterResponse, AdvancedFilterMethodsResponse } from 'types/api/advancedFilter';
import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
diff --git a/lib/getFilterValuesFromQuery.ts b/lib/getFilterValuesFromQuery.ts
index ce5eddeb63..1ba9243d63 100644
--- a/lib/getFilterValuesFromQuery.ts
+++ b/lib/getFilterValuesFromQuery.ts
@@ -1,14 +1,10 @@
+import getValuesArrayFromQuery from './getValuesArrayFromQuery';
+
export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined) {
- if (val === undefined) {
- return;
- }
+ const valArray = getValuesArrayFromQuery(val);
- const valArray = [];
- if (typeof val === 'string') {
- valArray.push(...val.split(','));
- }
- if (Array.isArray(val)) {
- val.forEach(el => valArray.push(...el.split(',')));
+ if (!valArray) {
+ return;
}
return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array;
diff --git a/lib/getValuesArrayFromQuery.ts b/lib/getValuesArrayFromQuery.ts
new file mode 100644
index 0000000000..647eb03cb3
--- /dev/null
+++ b/lib/getValuesArrayFromQuery.ts
@@ -0,0 +1,18 @@
+export default function getValuesArrayFromQuery(val: string | Array | undefined) {
+ if (val === undefined) {
+ return;
+ }
+
+ const valArray = [];
+ if (typeof val === 'string') {
+ valArray.push(...val.split(','));
+ }
+ if (Array.isArray(val)) {
+ if (!val.length) {
+ return;
+ }
+ val.forEach(el => valArray.push(...el.split(',')));
+ }
+
+ return valArray;
+}
diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts
index 868872bd02..48ba6565b1 100644
--- a/nextjs/nextjs-routes.d.ts
+++ b/nextjs/nextjs-routes.d.ts
@@ -15,8 +15,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/accounts">
| DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
- | StaticRoute<"/api/config">
| StaticRoute<"/advanced-filter">
+ | StaticRoute<"/api/config">
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/healthz">
| StaticRoute<"/api/log">
diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts
index eb787ac206..b0031ff8eb 100644
--- a/public/icons/name.d.ts
+++ b/public/icons/name.d.ts
@@ -33,6 +33,7 @@
| "clock"
| "coins/bitcoin"
| "collection"
+ | "columns"
| "contracts/proxy"
| "contracts/regular_many"
| "contracts/regular"
diff --git a/types/api/advancedFilter.ts b/types/api/advancedFilter.ts
index aae4b21b5e..d6ace508c9 100644
--- a/types/api/advancedFilter.ts
+++ b/types/api/advancedFilter.ts
@@ -4,6 +4,7 @@ import type { TokenInfo } from './token';
export type AdvancedFilterParams = {
tx_types?: Array;
methods?: Array;
+ methods_names?: Array; /* frontend only */
age_from?: string;
age_to?: string;
age?: AdvancedFilterAge; /* frontend only */
@@ -16,6 +17,8 @@ export type AdvancedFilterParams = {
amount_to?: string;
token_contract_address_hashes_to_include?: Array;
token_contract_address_hashes_to_exclude?: Array;
+ token_contract_symbols_to_include?: Array;
+ token_contract_symbols_to_exclude?: Array;
};
export const ADVANCED_FILTER_TYPES = [ 'coin_transfer', 'ERC-20', 'ERC-404', 'ERC-721', 'ERC-1155' ] as const;
@@ -42,7 +45,7 @@ export type AdvancedFilterResponseItem = {
}
export type AdvancedFiltersSearchParams = {
- methods: Record;
+ methods: Record;
tokens: Record;
}
diff --git a/ui/address/mud/AddressMudRecordsKeyFilter.tsx b/ui/address/mud/AddressMudRecordsKeyFilter.tsx
index 43e24c85af..d58c1dfcf2 100644
--- a/ui/address/mud/AddressMudRecordsKeyFilter.tsx
+++ b/ui/address/mud/AddressMudRecordsKeyFilter.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import FilterInput from 'ui/shared/filters/FilterInput';
-import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
+import TableColumnFilterWrapper from 'ui/shared/filters/TableColumnFilterWrapper';
+
+import AddressMudRecordsKeyFilterContent from './AddressMudRecordsKeyFilterContent';
type Props = {
value?: string;
@@ -12,29 +13,20 @@ type Props = {
}
const AddressMudRecordsKeyFilter = ({ value = '', handleFilterChange, columnName, title, isLoading }: Props) => {
- const [ filterValue, setFilterValue ] = React.useState(value);
-
- const onFilter = React.useCallback(() => {
- handleFilterChange(filterValue);
- }, [ handleFilterChange, filterValue ]);
-
return (
-
-
-
+
);
};
diff --git a/ui/address/mud/AddressMudRecordsKeyFilterContent.tsx b/ui/address/mud/AddressMudRecordsKeyFilterContent.tsx
new file mode 100644
index 0000000000..b27cc1dea1
--- /dev/null
+++ b/ui/address/mud/AddressMudRecordsKeyFilterContent.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import FilterInput from 'ui/shared/filters/FilterInput';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
+
+type Props = {
+ value?: string;
+ handleFilterChange: (val: string) => void;
+ title: string;
+ columnName: string;
+ onClose?: () => void;
+}
+
+const AddressMudRecordsKeyFilter = ({ value = '', handleFilterChange, columnName, title, onClose }: Props) => {
+ const [ filterValue, setFilterValue ] = React.useState(value);
+
+ const onFilter = React.useCallback(() => {
+ handleFilterChange(filterValue);
+ }, [ handleFilterChange, filterValue ]);
+
+ return (
+
+
+
+ );
+};
+
+export default AddressMudRecordsKeyFilter;
diff --git a/ui/advancedFilter/ColumnFilterWrapper.tsx b/ui/advancedFilter/ColumnFilterWrapper.tsx
index 95c4eecefe..184ff04def 100644
--- a/ui/advancedFilter/ColumnFilterWrapper.tsx
+++ b/ui/advancedFilter/ColumnFilterWrapper.tsx
@@ -1,5 +1,4 @@
import {
- Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
@@ -9,6 +8,7 @@ import {
} from '@chakra-ui/react';
import React from 'react';
+import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
diff --git a/ui/advancedFilter/ColumnsButton.tsx b/ui/advancedFilter/ColumnsButton.tsx
index ec16b3d6a9..06b4840f2d 100644
--- a/ui/advancedFilter/ColumnsButton.tsx
+++ b/ui/advancedFilter/ColumnsButton.tsx
@@ -1,7 +1,6 @@
import {
Button,
Grid,
- Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
@@ -13,6 +12,8 @@ import type { ChangeEvent } from 'react';
import type { ColumnsIds } from 'ui/pages/AdvancedFilter';
import { TABLE_COLUMNS } from 'ui/pages/AdvancedFilter';
+import Popover from 'ui/shared/chakra/Popover';
+import IconSvg from 'ui/shared/IconSvg';
interface Props {
columns: Record;
@@ -36,8 +37,11 @@ const ColumnsButton = ({ columns, onChange }: Props) => {
onClick={ onToggle }
variant="outline"
colorScheme="gray"
+ size="sm"
+ leftIcon={ }
// isLoading={ isLoading }
>
+ { /* */ }
Columns
diff --git a/ui/advancedFilter/FilterByColumn.tsx b/ui/advancedFilter/FilterByColumn.tsx
index d01636c76b..9d45eeed65 100644
--- a/ui/advancedFilter/FilterByColumn.tsx
+++ b/ui/advancedFilter/FilterByColumn.tsx
@@ -3,7 +3,9 @@ import React from 'react';
import type { AdvancedFilterParams, AdvancedFiltersSearchParams } from 'types/api/advancedFilter';
import type { ColumnsIds } from 'ui/pages/AdvancedFilter';
+import TableColumnFilterWrapper from 'ui/shared/filters/TableColumnFilterWrapper';
+import { NATIVE_TOKEN } from './constants';
import type { AddressFilterMode } from './filters/AddressFilter';
import AddressFilter from './filters/AddressFilter';
import AddressRelationFilter from './filters/AddressRelationFilter';
@@ -22,26 +24,75 @@ type Props = {
handleFilterChange: (field: keyof AdvancedFilterParams, val: unknown) => void;
isLoading?: boolean;
}
+
const FilterByColumn = ({ column, filters, columnName, handleFilterChange, searchParams, isLoading }: Props) => {
const commonProps = { columnName, handleFilterChange, isLoading };
switch (column) {
- case 'type':
- return ;
+ case 'type': {
+ const value = filters.tx_types;
+ return (
+
+
+
+ );
+ }
case 'method': {
- const value = filters.methods?.map(m => searchParams?.methods[m] || { method_id: m });
- return ;
+ const value = filters.methods?.map(m => ({ name: searchParams?.methods[m], method_id: m }));
+ return (
+
+
+
+ );
+ }
+ case 'age': {
+ const value = { age: filters.age, from: filters.age_from, to: filters.age_to };
+ return (
+
+
+
+ );
+ }
+ case 'or_and': {
+ return (
+
+
+
+ );
}
- case 'age':
- return ;
- case 'or_and':
- return ;
case 'from': {
const valueInclude = filters?.from_address_hashes_to_include?.map(hash => ({ address: hash, mode: 'include' as AddressFilterMode }));
const valueExclude = filters?.from_address_hashes_to_exclude?.map(hash => ({ address: hash, mode: 'exclude' as AddressFilterMode }));
const value = (valueInclude || []).concat(valueExclude || []);
-
- return ;
+ return (
+
+
+
+ );
}
case 'to': {
@@ -49,11 +100,30 @@ const FilterByColumn = ({ column, filters, columnName, handleFilterChange, searc
const valueExclude = filters?.to_address_hashes_to_exclude?.map(hash => ({ address: hash, mode: 'exclude' as AddressFilterMode }));
const value = (valueInclude || []).concat(valueExclude || []);
-
- return ;
+ return (
+
+
+
+ );
+ }
+ case 'amount': {
+ const value = { from: filters.amount_from, to: filters.amount_to };
+ return (
+
+
+
+ );
}
- case 'amount':
- return ;
case 'asset': {
const tokens = searchParams?.tokens;
@@ -64,11 +134,26 @@ const FilterByColumn = ({ column, filters, columnName, handleFilterChange, searc
'exclude' as AssetFilterMode;
return ({ token, mode });
}) : [];
-
- return ;
+ if (filters.token_contract_address_hashes_to_include?.includes('native')) {
+ value.unshift({ token: NATIVE_TOKEN, mode: 'include' });
+ }
+ if (filters.token_contract_address_hashes_to_exclude?.includes('native')) {
+ value.unshift({ token: NATIVE_TOKEN, mode: 'exclude' });
+ }
+ return (
+
+
+
+ );
}
- default:
+ default: {
return null;
+ }
}
};
diff --git a/ui/advancedFilter/ItemByColumn.tsx b/ui/advancedFilter/ItemByColumn.tsx
index 00526bda27..10b5445c69 100644
--- a/ui/advancedFilter/ItemByColumn.tsx
+++ b/ui/advancedFilter/ItemByColumn.tsx
@@ -1,16 +1,17 @@
-import { Skeleton } from '@chakra-ui/react';
+import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AdvancedFilterResponseItem } from 'types/api/advancedFilter';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import getCurrencyValue from 'lib/getCurrencyValue';
import type { ColumnsIds } from 'ui/pages/AdvancedFilter';
+import AddressFromToIcon from 'ui/shared/address/AddressFromToIcon';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import { ADVANCED_FILTER_TYPES } from './constants';
@@ -22,7 +23,7 @@ type Props = {
const ItemByColumn = ({ item, column, isLoading }: Props) => {
switch (column) {
case 'tx_hash':
- return ;
+ return ;
case 'type': {
const type = ADVANCED_FILTER_TYPES.find(t => t.id === item.type);
if (!type) {
@@ -31,14 +32,25 @@ const ItemByColumn = ({ item, column, isLoading }: Props) => {
return { type.name };
}
case 'method':
- return item.method ? { item.method } : null;
+ return item.method ? { item.method } : null;
case 'age':
- return { dayjs(item.timestamp).fromNow() };
+ return ;
case 'from':
- return ;
+ return (
+
+
+
+
+ );
case 'to':
return ;
case 'amount': {
+ if (item.token?.type === 'ERC-721') {
+ return 1;
+ }
if (item.total) {
return (
@@ -57,8 +69,8 @@ const ItemByColumn = ({ item, column, isLoading }: Props) => {
}
case 'asset':
return item.token ?
- :
- { `${ config.chain.currency.name } (${ config.chain.currency.symbol })` };
+ :
+ { config.chain.currency.symbol };
case 'fee':
return { item.fee ? getCurrencyValue({ value: item.fee, accuracy: 8 }).valueStr : '-' };
default:
diff --git a/ui/advancedFilter/constants.ts b/ui/advancedFilter/constants.ts
index 6b361b4490..5dc1555b98 100644
--- a/ui/advancedFilter/constants.ts
+++ b/ui/advancedFilter/constants.ts
@@ -1,3 +1,7 @@
+import type { TokenInfo } from 'types/api/token';
+
+import config from 'configs/app';
+
export const ADVANCED_FILTER_TYPES = [
{
id: 'coin_transfer',
@@ -21,7 +25,6 @@ export const ADVANCED_FILTER_TYPES = [
},
] as const;
-//???
export const ADVANCED_FILTER_TYPES_WITH_ALL = [
{
id: 'all',
@@ -29,3 +32,11 @@ export const ADVANCED_FILTER_TYPES_WITH_ALL = [
},
...ADVANCED_FILTER_TYPES,
];
+
+export const NATIVE_TOKEN = {
+ name: config.chain.currency.name || '',
+ icon_url: '',
+ symbol: config.chain.currency.symbol || '',
+ address: 'native',
+ type: 'ERC-20' as const,
+} as TokenInfo;
diff --git a/ui/advancedFilter/filters/AddressFilter.tsx b/ui/advancedFilter/filters/AddressFilter.tsx
index 5d88714686..e0842489d3 100644
--- a/ui/advancedFilter/filters/AddressFilter.tsx
+++ b/ui/advancedFilter/filters/AddressFilter.tsx
@@ -5,10 +5,9 @@ import React from 'react';
import type { AdvancedFilterParams } from 'types/api/advancedFilter';
import ClearButton from 'ui/shared/ClearButton';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
import IconSvg from 'ui/shared/IconSvg';
-import ColumnFilter from '../ColumnFilter';
-
const FILTER_PARAM_TO_INCLUDE = 'to_address_hashes_to_include';
const FILTER_PARAM_FROM_INCLUDE = 'from_address_hashes_to_include';
const FILTER_PARAM_TO_EXCLUDE = 'to_address_hashes_to_exclude';
@@ -24,6 +23,7 @@ type Props = {
columnName: string;
type: 'from' | 'to';
isLoading?: boolean;
+ onClose?: () => void;
}
type InputProps = {
@@ -75,7 +75,7 @@ const AddressFilterInput = ({ address, mode, onModeChange, onRemove, onChange, i
const emptyItem = { address: '', mode: 'include' as AddressFilterMode };
-const AddressFilter = ({ type, value, handleFilterChange, columnName, isLoading }: Props) => {
+const AddressFilter = ({ type, value, handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] =
React.useState>([ ...value, emptyItem ] || [ emptyItem ]);
@@ -121,15 +121,12 @@ const AddressFilter = ({ type, value, handleFilterChange, columnName, isLoading
}, [ handleFilterChange, currentValue, type ]);
return (
-
{ currentValue.map((item, index) => (
@@ -145,7 +142,7 @@ const AddressFilter = ({ type, value, handleFilterChange, columnName, isLoading
/>
)) }
-
+
);
};
diff --git a/ui/advancedFilter/filters/AddressRelationFilter.tsx b/ui/advancedFilter/filters/AddressRelationFilter.tsx
index 37859ea871..7a4a6b00e7 100644
--- a/ui/advancedFilter/filters/AddressRelationFilter.tsx
+++ b/ui/advancedFilter/filters/AddressRelationFilter.tsx
@@ -1,10 +1,8 @@
-import { Radio, RadioGroup, Stack } from '@chakra-ui/react';
+import { Radio, RadioGroup, Stack, Box } from '@chakra-ui/react';
import React from 'react';
import { type AdvancedFilterParams } from 'types/api/advancedFilter';
-import ColumnFilterWrapper from '../ColumnFilterWrapper';
-
const FILTER_PARAM = 'address_relation';
type Value = 'or' | 'and';
@@ -19,27 +17,21 @@ type Props = {
onClose?: () => void;
}
-const AddressRelationFilter = ({ value = DEFAULT_VALUE, handleFilterChange, columnName, onClose, isLoading }: Props) => {
-
+const AddressRelationFilter = ({ value = DEFAULT_VALUE, handleFilterChange, onClose }: Props) => {
const onFilter = React.useCallback((val: Value) => {
onClose && onClose();
handleFilterChange(FILTER_PARAM, val);
}, [ handleFilterChange, onClose ]);
return (
-
+
OR
AND
-
+
);
};
diff --git a/ui/advancedFilter/filters/AgeFilter.tsx b/ui/advancedFilter/filters/AgeFilter.tsx
index 95a34d0f6a..cbd543f177 100644
--- a/ui/advancedFilter/filters/AgeFilter.tsx
+++ b/ui/advancedFilter/filters/AgeFilter.tsx
@@ -6,8 +6,8 @@ import { ADVANCED_FILTER_AGES, type AdvancedFilterAge, type AdvancedFilterParams
import dayjs from 'lib/date/dayjs';
import { ndash } from 'lib/html-entities';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
-import ColumnFilter from '../ColumnFilter';
import { getDurationFromAge } from '../lib';
const FILTER_PARAM_FROM = 'age_from';
@@ -22,9 +22,10 @@ type Props = {
handleFilterChange: (filed: keyof AdvancedFilterParams, value?: string) => void;
columnName: string;
isLoading?: boolean;
+ onClose?: () => void;
}
-const AgeFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Props) => {
+const AgeFilter = ({ value = {}, handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] = React.useState(value || defaultValue);
const handleFromChange = React.useCallback((event: ChangeEvent) => {
@@ -53,15 +54,12 @@ const AgeFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Pr
}, [ handleFilterChange, currentValue ]);
return (
-
{ ADVANCED_FILTER_AGES.map(val => (
@@ -82,7 +80,7 @@ const AgeFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Pr
{ ndash }
-
+
);
};
diff --git a/ui/advancedFilter/filters/AmountFilter.tsx b/ui/advancedFilter/filters/AmountFilter.tsx
index 92f34b4760..260ad3d108 100644
--- a/ui/advancedFilter/filters/AmountFilter.tsx
+++ b/ui/advancedFilter/filters/AmountFilter.tsx
@@ -5,8 +5,7 @@ import React from 'react';
import type { AdvancedFilterParams } from 'types/api/advancedFilter';
import { ndash } from 'lib/html-entities';
-
-import ColumnFilter from '../ColumnFilter';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
const FILTER_PARAM_FROM = 'amount_from';
const FILTER_PARAM_TO = 'amount_to';
@@ -44,11 +43,10 @@ type AmountValue = { from?: string; to?: string }
type Props = {
value?: AmountValue;
handleFilterChange: (filed: keyof AdvancedFilterParams, value?: string) => void;
- columnName: string;
- isLoading?: boolean;
+ onClose?: () => void;
}
-const AmountFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Props) => {
+const AmountFilter = ({ value = {}, handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] = React.useState(value || defaultValue);
const handleFromChange = React.useCallback((event: ChangeEvent) => {
@@ -72,15 +70,12 @@ const AmountFilter = ({ value = {}, handleFilterChange, columnName, isLoading }:
}, []);
return (
-
{ PRESETS.map(preset => (
@@ -104,7 +99,7 @@ const AmountFilter = ({ value = {}, handleFilterChange, columnName, isLoading }:
{ ndash }
-
+
);
};
diff --git a/ui/advancedFilter/filters/AssetFilter.tsx b/ui/advancedFilter/filters/AssetFilter.tsx
index b19c176c3f..ceba2a2e00 100644
--- a/ui/advancedFilter/filters/AssetFilter.tsx
+++ b/ui/advancedFilter/filters/AssetFilter.tsx
@@ -9,11 +9,14 @@ import Tag from 'ui/shared/chakra/Tag';
import ClearButton from 'ui/shared/ClearButton';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import FilterInput from 'ui/shared/filters/FilterInput';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
-import ColumnFilter from '../ColumnFilter';
+import { NATIVE_TOKEN } from '../constants';
const FILTER_PARAM_INCLUDE = 'token_contract_address_hashes_to_include';
const FILTER_PARAM_EXCLUDE = 'token_contract_address_hashes_to_exclude';
+const NAME_PARAM_INCLUDE = 'token_contract_symbols_to_include';
+const NAME_PARAM_EXCLUDE = 'token_contract_symbols_to_exclude';
export type AssetFilterMode = 'include' | 'exclude';
@@ -25,9 +28,10 @@ type Props = {
handleFilterChange: (filed: keyof AdvancedFilterParams, val: Array) => void;
columnName: string;
isLoading?: boolean;
+ onClose?: () => void;
}
-const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props) => {
+const AssetFilter = ({ value, handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] = React.useState(value || []);
const [ searchTerm, setSearchTerm ] = React.useState('');
@@ -56,7 +60,12 @@ const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props
});
}, []);
- const tokensQuery = useApiQuery('tokens', { queryParams: { limit: '7', q: searchTerm } });
+ const tokensQuery = useApiQuery('tokens', {
+ queryParams: { limit: '7', q: searchTerm },
+ queryOptions: {
+ refetchOnMount: false,
+ },
+ });
const onTokenClick = React.useCallback((token: TokenInfo) => () => {
setCurrentValue(prev => prev.findIndex(i => i.token.address === token.address) > -1 ? prev : [ { token, mode: 'include' }, ...prev ]);
@@ -67,20 +76,19 @@ const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props
const onFilter = React.useCallback(() => {
setSearchTerm('');
handleFilterChange(FILTER_PARAM_INCLUDE, currentValue.filter(i => i.mode === 'include').map(i => i.token.address));
+ handleFilterChange(NAME_PARAM_INCLUDE, currentValue.filter(i => i.mode === 'include').map(i => i.token.symbol || ''));
handleFilterChange(FILTER_PARAM_EXCLUDE, currentValue.filter(i => i.mode === 'exclude').map(i => i.token.address));
+ handleFilterChange(NAME_PARAM_EXCLUDE, currentValue.filter(i => i.mode === 'exclude').map(i => i.token.symbol || ''));
return;
}, [ handleFilterChange, currentValue ]);
return (
-
Popular
- { tokensQuery.data.items.map(token => (
+ { [ NATIVE_TOKEN, ...tokensQuery.data.items ].map(token => (
@@ -138,11 +144,13 @@ const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props
id={ token.address }
onChange={ onTokenClick(token) }
overflow="hidden"
+ w="100%"
sx={{
'.chakra-checkbox__label': {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
+ flexGrow: 1,
},
}}
>
@@ -153,7 +161,7 @@ const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props
) }
-
+
);
};
diff --git a/ui/advancedFilter/filters/MethodFilter.tsx b/ui/advancedFilter/filters/MethodFilter.tsx
index 3d85827b62..b64ee2d3b5 100644
--- a/ui/advancedFilter/filters/MethodFilter.tsx
+++ b/ui/advancedFilter/filters/MethodFilter.tsx
@@ -9,21 +9,20 @@ import type { AdvancedFilterMethodInfo, AdvancedFilterParams } from 'types/api/a
import useApiQuery from 'lib/api/useApiQuery';
import Tag from 'ui/shared/chakra/Tag';
import FilterInput from 'ui/shared/filters/FilterInput';
-
-import ColumnFilter from '../ColumnFilter';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
const RESET_VALUE = 'all';
const FILTER_PARAM = 'methods';
+const NAMES_PARAM = 'methods_names';
type Props = {
value?: Array;
handleFilterChange: (filed: keyof AdvancedFilterParams, val: Array) => void;
- columnName: string;
- isLoading?: boolean;
+ onClose?: () => void;
}
-const MethodFilter = ({ value = [], handleFilterChange, columnName, isLoading }: Props) => {
+const MethodFilter = ({ value = [], handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] = React.useState>(value);
const [ searchTerm, setSearchTerm ] = React.useState('');
const [ methodsList, setMethodsList ] = React.useState>([]);
@@ -32,8 +31,10 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName, isLoading }:
setSearchTerm(value);
}, []);
- // q should work whem Max replace method /search with common one
- const methodsQuery = useApiQuery('advanced_filter_methods', { queryParams: { q: searchTerm } });
+ const methodsQuery = useApiQuery('advanced_filter_methods', {
+ queryParams: { q: searchTerm },
+ queryOptions: { refetchOnMount: false },
+ });
React.useEffect(() => {
if (!methodsList.length && methodsQuery.data) {
setMethodsList([ ...differenceBy(value, methodsQuery.data, i => i.method_id), ...methodsQuery.data ]);
@@ -61,18 +62,16 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName, isLoading }:
const onFilter = React.useCallback(() => {
handleFilterChange(FILTER_PARAM, currentValue.map(item => item.method_id));
+ handleFilterChange(NAMES_PARAM, currentValue.map(item => item.name || ''));
}, [ handleFilterChange, currentValue ]);
return (
-
{ methodsQuery.isLoading && }
- { /* fixme */ }
- { methodsQuery.isError && error }
+ { methodsQuery.isError && Something went wrong. Please try again. }
+ { Boolean(searchTerm) && methodsQuery.data?.length === 0 && No results found. }
{ methodsQuery.data && (
i.method_id) : [ 'all' ] }>
@@ -111,7 +110,7 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName, isLoading }:
) }
-
+
);
};
diff --git a/ui/advancedFilter/filters/TypeFilter.tsx b/ui/advancedFilter/filters/TypeFilter.tsx
index 3482a29182..f3526098f9 100644
--- a/ui/advancedFilter/filters/TypeFilter.tsx
+++ b/ui/advancedFilter/filters/TypeFilter.tsx
@@ -5,7 +5,8 @@ import React from 'react';
import type { AdvancedFilterParams, AdvancedFilterType } from 'types/api/advancedFilter';
-import ColumnFilter from '../ColumnFilter';
+import TableColumnFilter from 'ui/shared/filters/TableColumnFilter';
+
import { ADVANCED_FILTER_TYPES_WITH_ALL } from '../constants';
const RESET_VALUE = 'all';
@@ -15,11 +16,10 @@ const FILTER_PARAM = 'tx_types';
type Props = {
value?: Array;
handleFilterChange: (filed: keyof AdvancedFilterParams, value: Array) => void;
- columnName: string;
- isLoading?: boolean;
+ onClose?: () => void;
}
-const TypeFilter = ({ value = [], handleFilterChange, columnName, isLoading }: Props) => {
+const TypeFilter = ({ value = [], handleFilterChange, onClose }: Props) => {
const [ currentValue, setCurrentValue ] = React.useState>(value);
const handleChange = React.useCallback((event: ChangeEvent) => {
@@ -39,14 +39,12 @@ const TypeFilter = ({ value = [], handleFilterChange, columnName, isLoading }: P
}, [ handleFilterChange, currentValue ]);
return (
-
@@ -62,7 +60,7 @@ const TypeFilter = ({ value = [], handleFilterChange, columnName, isLoading }: P
)) }
-
+
);
};
diff --git a/ui/advancedFilter/lib.ts b/ui/advancedFilter/lib.ts
index 5e30bdc120..f485dfbb76 100644
--- a/ui/advancedFilter/lib.ts
+++ b/ui/advancedFilter/lib.ts
@@ -1,7 +1,11 @@
-import type { AdvancedFilterAge } from 'types/api/advancedFilter';
+import castArray from 'lodash/castArray';
+
+import type { AdvancedFilterAge, AdvancedFilterParams } from 'types/api/advancedFilter';
import { HOUR, DAY, MONTH } from 'lib/consts';
+import { ADVANCED_FILTER_TYPES } from './constants';
+
export function getDurationFromAge(age: AdvancedFilterAge) {
switch (age) {
case '1h':
@@ -18,3 +22,83 @@ export function getDurationFromAge(age: AdvancedFilterAge) {
return MONTH * 6;
}
}
+
+function getFilterValueWithNames(values?: Array, names?: Array) {
+ if (!names) {
+ return castArray(values).join(', ');
+ } else if (Array.isArray(names) && Array.isArray(values)) {
+ return names.map((n, i) => n ? n : values[i]).join(', ');
+ } else {
+ return names;
+ }
+}
+
+const filterParamNames: Record = {
+ // we don't show address_relation as filter tag
+ address_relation: '',
+ age: 'Age',
+ age_from: 'Date from',
+ age_to: 'Date to',
+ amount_from: 'Amount from',
+ amount_to: 'Amount to',
+ from_address_hashes_to_exclude: 'From Exc',
+ from_address_hashes_to_include: 'From',
+ methods: 'Methods',
+ methods_names: '',
+ to_address_hashes_to_exclude: 'To Exc',
+ to_address_hashes_to_include: 'To',
+ token_contract_address_hashes_to_exclude: 'Asset Exc',
+ token_contract_symbols_to_exclude: 'Asset Exc',
+ token_contract_address_hashes_to_include: 'Asset',
+ token_contract_symbols_to_include: 'Asset',
+ tx_types: 'Type',
+};
+
+export function getFilterTags(filters: AdvancedFilterParams) {
+ const filtersToShow = { ...filters };
+ if (filtersToShow.age) {
+ filtersToShow.age_from = undefined;
+ filtersToShow.age_to = undefined;
+ }
+
+ return (Object.entries(filtersToShow) as Array<[keyof AdvancedFilterParams, AdvancedFilterParams[keyof AdvancedFilterParams]]>).map(([ key, value ]) => {
+ if (!value) {
+ return;
+ }
+ const name = filterParamNames[key as keyof AdvancedFilterParams];
+ if (!name) {
+ return;
+ }
+ let valueStr;
+ switch (key) {
+ case 'methods': {
+ valueStr = getFilterValueWithNames(filtersToShow.methods, filtersToShow.methods_names);
+ break;
+ }
+ case 'tx_types': {
+ valueStr = castArray(value).map(i => ADVANCED_FILTER_TYPES.find(t => t.id === i)?.name).filter(Boolean).join(', ');
+ break;
+ }
+ case 'token_contract_address_hashes_to_exclude': {
+ valueStr = getFilterValueWithNames(filtersToShow.token_contract_address_hashes_to_exclude, filtersToShow.token_contract_symbols_to_exclude);
+ break;
+ }
+ case 'token_contract_address_hashes_to_include': {
+ valueStr = getFilterValueWithNames(filtersToShow.token_contract_address_hashes_to_include, filtersToShow.token_contract_symbols_to_include);
+ break;
+ }
+ default: {
+ valueStr = castArray(value).join(', ');
+ }
+ }
+ if (!valueStr) {
+ return;
+ }
+
+ return {
+ key: key as keyof AdvancedFilterParams,
+ name,
+ value: valueStr,
+ };
+ }).filter(Boolean);
+}
diff --git a/ui/pages/AdvancedFilter.tsx b/ui/pages/AdvancedFilter.tsx
index d45a9b7087..4ecd5f47ce 100644
--- a/ui/pages/AdvancedFilter.tsx
+++ b/ui/pages/AdvancedFilter.tsx
@@ -1,5 +1,4 @@
-import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
-import castArray from 'lodash/castArray';
+import { Table, Tbody, Tr, Th, Td, Thead, Box, Text, Tag, TagCloseButton, chakra, Flex, TagLabel, HStack, Link } from '@chakra-ui/react';
import omit from 'lodash/omit';
import { useRouter } from 'next/router';
import React from 'react';
@@ -7,73 +6,80 @@ import React from 'react';
import type { AdvancedFilterParams } from 'types/api/advancedFilter';
import { ADVANCED_FILTER_TYPES, ADVANCED_FILTER_AGES } from 'types/api/advancedFilter';
+import useApiQuery from 'lib/api/useApiQuery';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import dayjs from 'lib/date/dayjs';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
+import getValuesArrayFromQuery from 'lib/getValuesArrayFromQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADVANCED_FILTER_ITEM } from 'stubs/advancedFilter';
import { generateListStub } from 'stubs/utils';
import ColumnsButton from 'ui/advancedFilter/ColumnsButton';
import FilterByColumn from 'ui/advancedFilter/FilterByColumn';
import ItemByColumn from 'ui/advancedFilter/ItemByColumn';
-import { getDurationFromAge } from 'ui/advancedFilter/lib';
+import { getDurationFromAge, getFilterTags } from 'ui/advancedFilter/lib';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
+import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
-import TheadSticky from 'ui/shared/TheadSticky';
export type ColumnsIds = 'tx_hash' | 'type' | 'method' | 'age' | 'from' | 'or_and' | 'to' | 'amount' | 'asset' | 'fee';
type TxTableColumn = {
id: ColumnsIds;
name: string;
+ width: string;
isNumeric?: boolean;
}
export const TABLE_COLUMNS: Array = [
{
id: 'tx_hash',
name: 'Tx hash',
+ width: '180px',
},
{
id: 'type',
name: 'Type',
+ width: '160px',
},
{
id: 'method',
name: 'Method',
-
+ width: '160px',
},
{
id: 'age',
name: 'Age',
+ width: '80px',
},
{
id: 'from',
name: 'From',
- },
- {
- id: 'or_and',
- name: 'OR/AND',
+ width: '190px',
},
{
id: 'to',
name: 'To',
+ width: '160px',
},
{
id: 'amount',
name: 'Amount',
isNumeric: true,
+ width: '150px',
},
{
id: 'asset',
name: 'Asset',
+ width: '120px',
},
{
id: 'fee',
name: 'Fee',
+ width: '120px',
},
] as const;
@@ -86,24 +92,21 @@ const AdvancedFilter = () => {
const age = getFilterValueFromQuery(ADVANCED_FILTER_AGES, router.query.age);
return {
tx_types: getFilterValuesFromQuery(ADVANCED_FILTER_TYPES, router.query.tx_types),
- methods: router.query.methods ? castArray(router.query.methods) : undefined,
+ methods: getValuesArrayFromQuery(router.query.methods),
+ methods_names: getValuesArrayFromQuery(router.query.methods_names),
amount_from: getQueryParamString(router.query.amount_from),
amount_to: getQueryParamString(router.query.amount_to),
age,
age_to: age ? dayjs().toISOString() : getQueryParamString(router.query.age_to),
age_from: age ? dayjs((dayjs().valueOf() - getDurationFromAge(age))).toISOString() : getQueryParamString(router.query.age_from),
- token_contract_address_hashes_to_exclude:
- router.query.token_contract_address_hashes_to_exclude ? castArray(router.query.token_contract_address_hashes_to_exclude) : undefined,
- token_contract_address_hashes_to_include:
- router.query.token_contract_address_hashes_to_include ? castArray(router.query.token_contract_address_hashes_to_include) : undefined,
- to_address_hashes_to_include:
- router.query.to_address_hashes_to_include ? castArray(router.query.to_address_hashes_to_include) : undefined,
- from_address_hashes_to_include:
- router.query.from_address_hashes_to_include ? castArray(router.query.from_address_hashes_to_include) : undefined,
- to_address_hashes_to_exclude:
- router.query.to_address_hashes_to_exclude ? castArray(router.query.to_address_hashes_to_exclude) : undefined,
- from_address_hashes_to_exclude:
- router.query.from_address_hashes_to_exclude ? castArray(router.query.from_address_hashes_to_exclude) : undefined,
+ token_contract_address_hashes_to_exclude: getValuesArrayFromQuery(router.query.token_contract_address_hashes_to_exclude),
+ token_contract_symbols_to_exclude: getValuesArrayFromQuery(router.query.token_contract_symbols_to_exclude),
+ token_contract_address_hashes_to_include: getValuesArrayFromQuery(router.query.token_contract_address_hashes_to_include),
+ token_contract_symbols_to_include: getValuesArrayFromQuery(router.query.token_contract_symbols_to_include),
+ to_address_hashes_to_include: getValuesArrayFromQuery(router.query.to_address_hashes_to_include),
+ from_address_hashes_to_include: getValuesArrayFromQuery(router.query.from_address_hashes_to_include),
+ to_address_hashes_to_exclude: getValuesArrayFromQuery(router.query.to_address_hashes_to_exclude),
+ from_address_hashes_to_exclude: getValuesArrayFromQuery(router.query.from_address_hashes_to_exclude),
};
});
@@ -132,6 +135,10 @@ const AdvancedFilter = () => {
},
});
+ // maybe don't need to prefetch, but on dev sepolia those requests take several seconds.
+ useApiQuery('tokens', { queryParams: { limit: '7', q: '' }, queryOptions: { refetchOnMount: false } });
+ useApiQuery('advanced_filter_methods', { queryParams: { q: '' }, queryOptions: { refetchOnMount: false } });
+
const handleFilterChange = React.useCallback((field: keyof AdvancedFilterParams, val: unknown) => {
setFilters(prevState => {
const newState = { ...prevState };
@@ -144,6 +151,24 @@ const AdvancedFilter = () => {
});
}, [ onFilterChange ]);
+ const onClearFilter = React.useCallback((key: keyof AdvancedFilterParams) => () => {
+ if (key === 'methods') {
+ handleFilterChange('methods_names', undefined);
+ }
+ if (key === 'token_contract_address_hashes_to_exclude') {
+ handleFilterChange('token_contract_symbols_to_exclude', undefined);
+ }
+ if (key === 'token_contract_address_hashes_to_include') {
+ handleFilterChange('token_contract_symbols_to_include', undefined);
+ }
+ handleFilterChange(key, undefined);
+ }, [ handleFilterChange ]);
+
+ const clearAllFilters = React.useCallback(() => {
+ setFilters({});
+ onFilterChange({});
+ }, [ onFilterChange ]);
+
const columnsToShow = React.useMemo(() => {
return TABLE_COLUMNS.filter(c => columns[c.id]);
}, [ columns ]);
@@ -152,40 +177,73 @@ const AdvancedFilter = () => {
return null;
}
+ const filterTags = getFilterTags(filters);
+
const content = (
-
-
-
- { columnsToShow.map(column => {
- return (
-
- { column.name }
-
- |
- );
- }) }
-
-
-
- { data?.items.map((item, index) => (
-
- { columnsToShow.map(column => (
-
-
- |
- )) }
+
+
+
+
+ { columnsToShow.map(column => {
+ return (
+
+ { column.name }
+
+ { column.id === 'from' && (
+ <>
+ OR/AND
+
+ >
+ ) }
+ |
+ );
+ }) }
- )) }
-
-
+
+
+ { data?.items.map((item, index) => (
+
+ { columnsToShow.map(column => (
+
+
+ |
+ )) }
+
+ )) }
+
+
+
);
@@ -202,16 +260,38 @@ const AdvancedFilter = () => {
title="Advanced filter"
withTextAd
/>
+ { filterTags.length !== 0 && (
+ <>
+
+ Filtered by:
+
+
+ Reset filters
+
+
+
+ { filterTags.map(t => (
+
+
+ { t.name }:
+ { t.value }
+
+
+
+ )) }
+
+ >
+ ) }
>
);
diff --git a/ui/shared/filters/TableColumnFilter.tsx b/ui/shared/filters/TableColumnFilter.tsx
index 2bcad20751..e89b510d63 100644
--- a/ui/shared/filters/TableColumnFilter.tsx
+++ b/ui/shared/filters/TableColumnFilter.tsx
@@ -7,9 +7,7 @@ import {
} from '@chakra-ui/react';
import React from 'react';
-import TableColumnFilterWrapper from './TableColumnFilterWrapper';
-
-type ContentProps = {
+type Props = {
title: string;
isFilled?: boolean;
hasReset?: boolean;
@@ -19,14 +17,7 @@ type ContentProps = {
children: React.ReactNode;
}
-type Props = ContentProps & {
- columnName: string;
- isActive?: boolean;
- isLoading?: boolean;
- className?: string;
-}
-
-const TableColumnFilterContent = ({ title, isFilled, hasReset, onFilter, onReset, onClose, children }: ContentProps) => {
+const TableColumnFilter = ({ title, isFilled, hasReset, onFilter, onReset, onClose, children }: Props) => {
const onFilterClick = React.useCallback(() => {
onClose && onClose();
onFilter();
@@ -60,17 +51,4 @@ const TableColumnFilterContent = ({ title, isFilled, hasReset, onFilter, onReset
);
};
-const TableColumnFilter = ({ columnName, isActive, className, isLoading, ...props }: Props) => {
- return (
-
-
-
- );
-};
-
export default chakra(TableColumnFilter);
diff --git a/ui/shared/filters/TableColumnFilterWrapper.tsx b/ui/shared/filters/TableColumnFilterWrapper.tsx
index bd67f93a7c..0828f39b4f 100644
--- a/ui/shared/filters/TableColumnFilterWrapper.tsx
+++ b/ui/shared/filters/TableColumnFilterWrapper.tsx
@@ -23,12 +23,12 @@ interface Props {
const TableColumnFilterWrapper = ({ columnName, isActive, className, children, isLoading }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
- const child = React.Children.only(children) as React.ReactElement & {
+ const content = React.Children.only(children) as React.ReactElement & {
ref?: React.Ref;
};
- const modifiedChildren = React.cloneElement(
- child,
+ const modifiedContent = React.cloneElement(
+ content,
{ onClose },
);
@@ -51,7 +51,7 @@ const TableColumnFilterWrapper = ({ columnName, isActive, className, children, i
- { modifiedChildren }
+ { modifiedContent }