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 ( - - ); - }) } - - - - { data?.items.map((item, index) => ( - - { columnsToShow.map(column => ( - - )) } + +
- { column.name } - -
- -
+ + + { 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 }