diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 40f31eae4e..13b9a3af0a 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -801,8 +801,10 @@ export const RESOURCES = { 'age_from' as const, 'age_to' as const, 'age' as const /* frontend only */, - 'from_address_hashes' as const, - 'to_address_hashes' as const, + 'from_address_hashes_to_include' as const, + 'from_address_hashes_to_exclude' as const, + 'to_address_hashes_to_include' as const, + 'to_address_hashes_to_exclude' as const, 'address_relation' as const, 'amount_from' as const, 'amount_to' as const, diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 66319f429e..3669650cd1 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -46,6 +46,7 @@ const OG_TYPE_DICT: Record = { '/name-domains/[name]': 'Regular page', '/validators': 'Root page', '/gas-tracker': 'Root page', + '/advanced-filter': 'Root page', // service routes, added only to make typescript happy '/login': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 08acaaf1c8..175f19c833 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -50,6 +50,7 @@ const TEMPLATE_MAP: Record = { '/name-domains/[name]': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE, '/gas-tracker': DEFAULT_TEMPLATE, + '/advanced-filter': DEFAULT_TEMPLATE, // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index b004af7f7d..e4244aaec3 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -46,6 +46,7 @@ const TEMPLATE_MAP: Record = { '/name-domains/[name]': '%network_name% %name% domain details', '/validators': '%network_name% validators list', '/gas-tracker': '%network_name% gas tracker - Current gas fees', + '/advanced-filter': '%network_name% advanced filter', // service routes, added only to make typescript happy '/login': '%network_name% login', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index e53554756b..2fa06f1ac2 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -44,6 +44,7 @@ export const PAGE_TYPE_DICT: Record = { '/name-domains/[name]': 'Domain details', '/validators': 'Validators list', '/gas-tracker': 'Gas tracker', + '/advanced-filter': 'Advanced filter', // service routes, added only to make typescript happy '/login': 'Login', diff --git a/stubs/advancedFilter.ts b/stubs/advancedFilter.ts new file mode 100644 index 0000000000..341280220b --- /dev/null +++ b/stubs/advancedFilter.ts @@ -0,0 +1,17 @@ +import type { AdvancedFilterResponseItem } from 'types/api/advancedFilter'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ADVANCED_FILTER_ITEM: AdvancedFilterResponseItem = { + fee: '215504444616317', + from: ADDRESS_PARAMS, + hash: TX_HASH, + method: 'approve', + timestamp: '2022-11-11T11:11:11.000000Z', + to: ADDRESS_PARAMS, + token: null, + total: null, + type: 'coin_transfer', + value: '42000420000000000000', +}; diff --git a/types/api/advancedFilter.ts b/types/api/advancedFilter.ts index b54a359b4b..aae4b21b5e 100644 --- a/types/api/advancedFilter.ts +++ b/types/api/advancedFilter.ts @@ -7,8 +7,10 @@ export type AdvancedFilterParams = { age_from?: string; age_to?: string; age?: AdvancedFilterAge; /* frontend only */ - from_address_hashes?: Array; - to_address_hashes?: Array; + from_address_hashes_to_include?: Array; + from_address_hashes_to_exclude?: Array; + to_address_hashes_to_include?: Array; + to_address_hashes_to_exclude?: Array; address_relation?: 'or' | 'and'; amount_from?: string; amount_to?: string; @@ -25,6 +27,7 @@ export type AdvancedFilterAge = typeof ADVANCED_FILTER_AGES[number]; export type AdvancedFilterResponseItem = { fee: string; from: AddressParam; + created_contract?: AddressParam; hash: string; method: string | null; timestamp: string; @@ -39,8 +42,8 @@ export type AdvancedFilterResponseItem = { } export type AdvancedFiltersSearchParams = { - methods: Array; - tokens: Array; + methods: Record; + tokens: Record; } export type AdvancedFilterResponse = { @@ -59,5 +62,5 @@ export type AdvancedFilterMethodsResponse = Array; export type AdvancedFilterMethodInfo = { method_id: string; - name: string; + name?: string; } diff --git a/ui/advancedFilter/ColumnFilter.tsx b/ui/advancedFilter/ColumnFilter.tsx index a85772a1ba..eba49e0774 100644 --- a/ui/advancedFilter/ColumnFilter.tsx +++ b/ui/advancedFilter/ColumnFilter.tsx @@ -1,10 +1,4 @@ import { - Popover, - PopoverTrigger, - PopoverContent, - PopoverBody, - useDisclosure, - IconButton, chakra, Flex, Text, @@ -13,63 +7,74 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import IconSvg from 'ui/shared/IconSvg'; +import ColumnFilterWrapper from './ColumnFilterWrapper'; -interface Props { +type Props = { columnName: string; title: string; isActive?: boolean; isFilled?: boolean; onFilter: () => void; - onReset: () => void; + onReset?: () => void; + onClose?: () => void; + isLoading?: boolean; className?: string; children: React.ReactNode; } -const ColumnFilter = ({ columnName, title, isActive, isFilled, onFilter, onReset, className, children }: Props) => { - const { isOpen, onToggle, onClose } = useDisclosure(); +type ContentProps = { + title: string; + isFilled?: boolean; + onFilter: () => void; + onReset?: () => void; + onClose?: () => void; + children: React.ReactNode; +} + +const ColumnFilterContent = ({ title, isFilled, onFilter, onReset, onClose, children }: ContentProps) => { + const onFilterClick = React.useCallback(() => { + onClose && onClose(); + onFilter(); + }, [ onClose, onFilter ]); + return ( + <> + + { title } + + Reset + + + { children } + + + ); +}; +const ColumnFilter = ({ columnName, isActive, className, isLoading, ...props }: Props) => { return ( - - - } - isActive={ isActive } - /> - - - - - - { title } - - Reset - - - { children } - - - - - + + + ); }; diff --git a/ui/advancedFilter/ColumnFilterWrapper.tsx b/ui/advancedFilter/ColumnFilterWrapper.tsx new file mode 100644 index 0000000000..95c4eecefe --- /dev/null +++ b/ui/advancedFilter/ColumnFilterWrapper.tsx @@ -0,0 +1,57 @@ +import { + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + useDisclosure, + IconButton, + chakra, +} from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + columnName: string; + isActive?: boolean; + isLoading?: boolean; + className?: string; + children: React.ReactNode; +} + +const ColumnFilterWrapper = ({ columnName, isActive, className, children, isLoading }: Props) => { + const { isOpen, onToggle, onClose } = useDisclosure(); + + const child = React.Children.only(children) as React.ReactElement & { + ref?: React.Ref; + }; + + const modifiedChildren = React.cloneElement( + child, + { onClose }, + ); + + return ( + + + } + isActive={ isActive } + isDisabled={ isLoading } + /> + + + + { modifiedChildren } + + + + ); +}; + +export default chakra(ColumnFilterWrapper); diff --git a/ui/advancedFilter/FilterByColumn.tsx b/ui/advancedFilter/FilterByColumn.tsx index 42887b20dc..d01636c76b 100644 --- a/ui/advancedFilter/FilterByColumn.tsx +++ b/ui/advancedFilter/FilterByColumn.tsx @@ -4,6 +4,9 @@ import type { AdvancedFilterParams, AdvancedFiltersSearchParams } from 'types/ap import type { ColumnsIds } from 'ui/pages/AdvancedFilter'; +import type { AddressFilterMode } from './filters/AddressFilter'; +import AddressFilter from './filters/AddressFilter'; +import AddressRelationFilter from './filters/AddressRelationFilter'; import AgeFilter from './filters/AgeFilter'; import AmountFilter from './filters/AmountFilter'; import type { AssetFilterMode } from './filters/AssetFilter'; @@ -17,32 +20,51 @@ type Props = { column: ColumnsIds; columnName: string; handleFilterChange: (field: keyof AdvancedFilterParams, val: unknown) => void; + isLoading?: boolean; } -const FilterByColumn = ({ column, filters, searchParams, columnName, handleFilterChange }: Props) => { - const commonProps = { columnName, handleFilterChange }; +const FilterByColumn = ({ column, filters, columnName, handleFilterChange, searchParams, isLoading }: Props) => { + const commonProps = { columnName, handleFilterChange, isLoading }; switch (column) { - case 'type': { + case 'type': return ; + case 'method': { + const value = filters.methods?.map(m => searchParams?.methods[m] || { method_id: m }); + return ; } - case 'method': - // fix value - return ; case 'age': return ; - // case 'from': - // return ; - // case 'to': - // 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 ; + + } + case 'to': { + const valueInclude = filters?.to_address_hashes_to_include?.map(hash => ({ address: hash, mode: 'include' as AddressFilterMode })); + const valueExclude = filters?.to_address_hashes_to_exclude?.map(hash => ({ address: hash, mode: 'exclude' as AddressFilterMode })); + + const value = (valueInclude || []).concat(valueExclude || []); + + return ; + } case 'amount': - // fix types return ; case 'asset': { const tokens = searchParams?.tokens; const value = tokens ? - Object.entries(tokens).map(([ address, token ]) => - ({ token, mode: filters.token_contract_address_hashes_to_include?.includes(address) ? 'include' as AssetFilterMode : 'exclude' as AssetFilterMode }), - ) : []; + Object.entries(tokens).map(([ address, token ]) => { + const mode = filters.token_contract_address_hashes_to_include?.find(i => i.toLowerCase() === address) ? + 'include' as AssetFilterMode : + 'exclude' as AssetFilterMode; + return ({ token, mode }); + }) : []; + return ; } default: diff --git a/ui/advancedFilter/ItemByColumn.tsx b/ui/advancedFilter/ItemByColumn.tsx index eaef64171c..00526bda27 100644 --- a/ui/advancedFilter/ItemByColumn.tsx +++ b/ui/advancedFilter/ItemByColumn.tsx @@ -1,4 +1,4 @@ -import { Text } from '@chakra-ui/react'; +import { Skeleton } from '@chakra-ui/react'; import React from 'react'; import type { AdvancedFilterResponseItem } from 'types/api/advancedFilter'; @@ -17,39 +17,50 @@ import { ADVANCED_FILTER_TYPES } from './constants'; type Props = { item: AdvancedFilterResponseItem; column: ColumnsIds; + isLoading?: boolean; } -const ItemByColumn = ({ item, column }: 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) { return null; } - return { type.name }; + 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 { dayjs(item.timestamp).fromNow() }; case 'from': - return ; + return ; case 'to': - return ; + return ; case 'amount': { if (item.total) { - return { getCurrencyValue({ value: item.total?.value, decimals: item.total.decimals, accuracy: 8 }).valueStr }; + return ( + + { getCurrencyValue({ value: item.total?.value, decimals: item.total.decimals, accuracy: 8 }).valueStr } + + ); } if (item.value) { - return { getCurrencyValue({ value: item.value, decimals: config.chain.currency.decimals.toString(), accuracy: 8 }).valueStr }; + return ( + + { getCurrencyValue({ value: item.value, decimals: config.chain.currency.decimals.toString(), accuracy: 8 }).valueStr } + + ); } return null; } case 'asset': - return item.token ? : { `${ config.chain.currency.name } (${ config.chain.currency.symbol })` }; + return item.token ? + : + { `${ config.chain.currency.name } (${ config.chain.currency.symbol })` }; case 'fee': - return { item.fee ? getCurrencyValue({ value: item.fee, accuracy: 8 }).valueStr : '-' }; + return { item.fee ? getCurrencyValue({ value: item.fee, accuracy: 8 }).valueStr : '-' }; default: return null; } diff --git a/ui/advancedFilter/filters/AddressFilter.tsx b/ui/advancedFilter/filters/AddressFilter.tsx new file mode 100644 index 0000000000..5d88714686 --- /dev/null +++ b/ui/advancedFilter/filters/AddressFilter.tsx @@ -0,0 +1,152 @@ +import { Flex, Select, Input, InputGroup, InputRightElement, VStack, IconButton } from '@chakra-ui/react'; +import type { ChangeEvent } from 'react'; +import React from 'react'; + +import type { AdvancedFilterParams } from 'types/api/advancedFilter'; + +import ClearButton from 'ui/shared/ClearButton'; +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'; +const FILTER_PARAM_FROM_EXCLUDE = 'from_address_hashes_to_exclude'; + +export type AddressFilterMode = 'include' | 'exclude'; + +type Value = Array<{ address: string; mode: AddressFilterMode }>; + +type Props = { + value: Value; + handleFilterChange: (filed: keyof AdvancedFilterParams, val: Array | undefined) => void; + columnName: string; + type: 'from' | 'to'; + isLoading?: boolean; +} + +type InputProps = { + address?: string; + mode?: AddressFilterMode; + isLast: boolean; + onModeChange: (event: ChangeEvent) => void; + onRemove: () => void; + onChange: (event: ChangeEvent) => void; + onAddFieldClick: () => void; +} + +const AddressFilterInput = ({ address, mode, onModeChange, onRemove, onChange, isLast, onAddFieldClick }: InputProps) => { + return ( + + + + + + + + + { isLast && ( + } + /> + ) } + + ); +}; + +const emptyItem = { address: '', mode: 'include' as AddressFilterMode }; + +const AddressFilter = ({ type, value, handleFilterChange, columnName, isLoading }: Props) => { + const [ currentValue, setCurrentValue ] = + React.useState>([ ...value, emptyItem ] || [ emptyItem ]); + + const handleModeSelectChange = React.useCallback((index: number) => (event: React.ChangeEvent) => { + const value = event.target.value as AddressFilterMode; + setCurrentValue(prev => { + prev[index].mode = value; + return [ ...prev ]; + }); + }, []); + + const handleAddressChange = React.useCallback((index: number) => (event: React.ChangeEvent) => { + const value = event.target.value; + + setCurrentValue(prev => { + const newVal = [ ...prev ]; + newVal[index] = { ...newVal[index], address: value }; + return newVal; + }); + }, []); + + const handleRemove = React.useCallback((index: number) => () => { + setCurrentValue(prev => { + prev.splice(index, 1); + return [ ...prev ]; + }); + }, []); + + const onAddFieldClick = React.useCallback(() => { + setCurrentValue(prev => [ ...prev, emptyItem ]); + }, []); + + const onReset = React.useCallback(() => setCurrentValue([ emptyItem ]), []); + + const onFilter = React.useCallback(() => { + const includeFilterParam = type === 'from' ? FILTER_PARAM_FROM_INCLUDE : FILTER_PARAM_TO_INCLUDE; + const excludeFilterParam = type === 'from' ? FILTER_PARAM_FROM_EXCLUDE : FILTER_PARAM_TO_EXCLUDE; + const includeValue = currentValue.filter(i => i.mode === 'include').map(i => i.address); + const excludeValue = currentValue.filter(i => i.mode === 'exclude').map(i => i.address); + + handleFilterChange(includeFilterParam, includeValue.length ? includeValue : undefined); + handleFilterChange(excludeFilterParam, excludeValue.length ? excludeValue : undefined); + }, [ handleFilterChange, currentValue, type ]); + + return ( + + + { currentValue.map((item, index) => ( + + )) } + + + ); +}; + +export default AddressFilter; diff --git a/ui/advancedFilter/filters/AddressRelationFilter.tsx b/ui/advancedFilter/filters/AddressRelationFilter.tsx new file mode 100644 index 0000000000..37859ea871 --- /dev/null +++ b/ui/advancedFilter/filters/AddressRelationFilter.tsx @@ -0,0 +1,46 @@ +import { Radio, RadioGroup, Stack } 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'; + +const DEFAULT_VALUE = 'or' as Value; + +type Props = { + value?: Value; + handleFilterChange: (filed: keyof AdvancedFilterParams, value?: string) => void; + columnName: string; + isLoading?: boolean; + onClose?: () => void; +} + +const AddressRelationFilter = ({ value = DEFAULT_VALUE, handleFilterChange, columnName, onClose, isLoading }: Props) => { + + const onFilter = React.useCallback((val: Value) => { + onClose && onClose(); + handleFilterChange(FILTER_PARAM, val); + }, [ handleFilterChange, onClose ]); + + return ( + + + + OR + AND + + + + ); +}; + +export default AddressRelationFilter; diff --git a/ui/advancedFilter/filters/AgeFilter.tsx b/ui/advancedFilter/filters/AgeFilter.tsx index 92d29bb8c8..95a34d0f6a 100644 --- a/ui/advancedFilter/filters/AgeFilter.tsx +++ b/ui/advancedFilter/filters/AgeFilter.tsx @@ -21,9 +21,10 @@ type Props = { value?: AgeFromToValue; handleFilterChange: (filed: keyof AdvancedFilterParams, value?: string) => void; columnName: string; + isLoading?: boolean; } -const AgeFilter = ({ value = {}, handleFilterChange, columnName }: Props) => { +const AgeFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Props) => { const [ currentValue, setCurrentValue ] = React.useState(value || defaultValue); const handleFromChange = React.useCallback((event: ChangeEvent) => { @@ -59,6 +60,7 @@ const AgeFilter = ({ value = {}, handleFilterChange, columnName }: Props) => { isActive={ Boolean(value.from || value.to || value.age) } onFilter={ onFilter } onReset={ onReset } + isLoading={ isLoading } w="382px" > diff --git a/ui/advancedFilter/filters/AmountFilter.tsx b/ui/advancedFilter/filters/AmountFilter.tsx index d89313bf2a..92f34b4760 100644 --- a/ui/advancedFilter/filters/AmountFilter.tsx +++ b/ui/advancedFilter/filters/AmountFilter.tsx @@ -45,9 +45,10 @@ type Props = { value?: AmountValue; handleFilterChange: (filed: keyof AdvancedFilterParams, value?: string) => void; columnName: string; + isLoading?: boolean; } -const AmountFilter = ({ value = {}, handleFilterChange, columnName }: Props) => { +const AmountFilter = ({ value = {}, handleFilterChange, columnName, isLoading }: Props) => { const [ currentValue, setCurrentValue ] = React.useState(value || defaultValue); const handleFromChange = React.useCallback((event: ChangeEvent) => { @@ -78,6 +79,7 @@ const AmountFilter = ({ value = {}, handleFilterChange, columnName }: Props) => isActive={ Boolean(value.from || value.to) } onFilter={ onFilter } onReset={ onReset } + isLoading={ isLoading } w="382px" > diff --git a/ui/advancedFilter/filters/AssetFilter.tsx b/ui/advancedFilter/filters/AssetFilter.tsx index 698e9fb38f..b19c176c3f 100644 --- a/ui/advancedFilter/filters/AssetFilter.tsx +++ b/ui/advancedFilter/filters/AssetFilter.tsx @@ -24,12 +24,19 @@ type Props = { value: Value; handleFilterChange: (filed: keyof AdvancedFilterParams, val: Array) => void; columnName: string; + isLoading?: boolean; } -const AssetFilter = ({ value, handleFilterChange, columnName }: Props) => { +const AssetFilter = ({ value, handleFilterChange, columnName, isLoading }: Props) => { const [ currentValue, setCurrentValue ] = React.useState(value || []); const [ searchTerm, setSearchTerm ] = React.useState(''); + React.useEffect(() => { + if (!currentValue.length && value.length) { + setCurrentValue(value); + } + }, [ value, currentValue.length ]); + const onSearchChange = React.useCallback((value: string) => { setSearchTerm(value); }, []); @@ -58,6 +65,7 @@ const AssetFilter = ({ value, handleFilterChange, columnName }: Props) => { const onReset = React.useCallback(() => setCurrentValue([]), []); const onFilter = React.useCallback(() => { + setSearchTerm(''); handleFilterChange(FILTER_PARAM_INCLUDE, currentValue.filter(i => i.mode === 'include').map(i => i.token.address)); handleFilterChange(FILTER_PARAM_EXCLUDE, currentValue.filter(i => i.mode === 'exclude').map(i => i.token.address)); return; @@ -71,6 +79,7 @@ const AssetFilter = ({ value, handleFilterChange, columnName }: Props) => { isActive={ Boolean(value.length) } onFilter={ onFilter } onReset={ onReset } + isLoading={ isLoading } w="382px" > ; handleFilterChange: (filed: keyof AdvancedFilterParams, val: Array) => void; columnName: string; + isLoading?: boolean; } -const MethodFilter = ({ value = [], handleFilterChange, columnName }: Props) => { +const MethodFilter = ({ value = [], handleFilterChange, columnName, isLoading }: Props) => { const [ currentValue, setCurrentValue ] = React.useState>(value); const [ searchTerm, setSearchTerm ] = React.useState(''); - const [ methodsList, setMethodsList ] = React.useState>(); + const [ methodsList, setMethodsList ] = React.useState>([]); const onSearchChange = React.useCallback((value: string) => { setSearchTerm(value); @@ -37,8 +35,8 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName }: Props) => // q should work whem Max replace method /search with common one const methodsQuery = useApiQuery('advanced_filter_methods', { queryParams: { q: searchTerm } }); React.useEffect(() => { - if (!methodsList && methodsQuery.data) { - setMethodsList([ ...difference(value, methodsQuery.data), ...methodsQuery.data ]); + if (!methodsList.length && methodsQuery.data) { + setMethodsList([ ...differenceBy(value, methodsQuery.data, i => i.method_id), ...methodsQuery.data ]); } }, [ methodsQuery.data, value, methodsList ]); @@ -51,7 +49,9 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName }: Props) => } else { const methodInfo = methodsQuery.data?.find(m => m.method_id === id); if (methodInfo) { - setCurrentValue(prev => checked ? [ ...prev, methodInfo ] : without(prev, methodInfo)); + setCurrentValue(prev => { + return checked ? [ ...prev, methodInfo ] : without(prev, methodInfo); + }); searchTerm && checked && setMethodsList(prev => [ methodInfo, ...(prev || []) ]); } } @@ -71,6 +71,7 @@ const MethodFilter = ({ value = [], handleFilterChange, columnName }: Props) => isFilled={ Boolean(currentValue.length) } onFilter={ onFilter } onReset={ onReset } + isLoading={ isLoading } w="350px" > }, }} > - { method.name } + { method.name || method.method_id } { method.method_id } diff --git a/ui/advancedFilter/filters/TypeFilter.tsx b/ui/advancedFilter/filters/TypeFilter.tsx index 35a6c47d79..3482a29182 100644 --- a/ui/advancedFilter/filters/TypeFilter.tsx +++ b/ui/advancedFilter/filters/TypeFilter.tsx @@ -16,9 +16,10 @@ type Props = { value?: Array; handleFilterChange: (filed: keyof AdvancedFilterParams, value: Array) => void; columnName: string; + isLoading?: boolean; } -const TypeFilter = ({ value = [], handleFilterChange, columnName }: Props) => { +const TypeFilter = ({ value = [], handleFilterChange, columnName, isLoading }: Props) => { const [ currentValue, setCurrentValue ] = React.useState>(value); const handleChange = React.useCallback((event: ChangeEvent) => { @@ -42,9 +43,10 @@ const TypeFilter = ({ value = [], handleFilterChange, columnName }: Props) => { columnName={ columnName } title="Type of transfer" isActive={ Boolean(value.length) } - isFilled={ Boolean(currentValue.length) } + isFilled={ true } onFilter={ onFilter } onReset={ onReset } + isLoading={ isLoading } > diff --git a/ui/pages/AdvancedFilter.tsx b/ui/pages/AdvancedFilter.tsx index a9883bb8a7..d45a9b7087 100644 --- a/ui/pages/AdvancedFilter.tsx +++ b/ui/pages/AdvancedFilter.tsx @@ -12,6 +12,8 @@ import dayjs from 'lib/date/dayjs'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; 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'; @@ -94,12 +96,40 @@ const AdvancedFilter = () => { 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, }; }); + const [ columns, setColumns ] = React.useState>(COLUMNS_CHECKED); - const { data, isError, isLoading, pagination, onFilterChange } = useQueryWithPages({ + const { data, isError, isLoading, pagination, onFilterChange, isPlaceholderData } = useQueryWithPages({ resourceName: 'advanced_filter', filters, + options: { + placeholderData: generateListStub<'advanced_filter'>( + ADVANCED_FILTER_ITEM, + 50, + { + next_page_params: { + block_number: 5867485, + internal_transaction_index: 0, + items_count: 50, + token_transfer_index: null, + transaction_index: 2, + }, + search_params: { + tokens: {}, + methods: {}, + }, + }, + ), + }, }); const handleFilterChange = React.useCallback((field: keyof AdvancedFilterParams, val: unknown) => { @@ -137,6 +167,7 @@ const AdvancedFilter = () => { handleFilterChange={ handleFilterChange } filters={ filters } searchParams={ data?.search_params } + isLoading={ isPlaceholderData } /> ); @@ -144,11 +175,11 @@ const AdvancedFilter = () => { - { data?.items.map(item => ( - + { data?.items.map((item, index) => ( + { columnsToShow.map(column => ( - + )) }