Skip to content

Commit

Permalink
advanced filters stage 2
Browse files Browse the repository at this point in the history
  • Loading branch information
isstuev committed May 10, 2024
1 parent 3907309 commit ecf11ae
Show file tree
Hide file tree
Showing 15 changed files with 457 additions and 103 deletions.
6 changes: 4 additions & 2 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,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,
Expand Down
17 changes: 17 additions & 0 deletions stubs/advancedFilter.ts
Original file line number Diff line number Diff line change
@@ -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',
};
12 changes: 7 additions & 5 deletions types/api/advancedFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ export type AdvancedFilterParams = {
age_from?: string;
age_to?: string;
age?: AdvancedFilterAge; /* frontend only */
from_address_hashes?: Array<string>;
to_address_hashes?: Array<string>;
from_address_hashes_to_include?: Array<string>;
from_address_hashes_to_exclude?: Array<string>;
to_address_hashes_to_include?: Array<string>;
to_address_hashes_to_exclude?: Array<string>;
address_relation?: 'or' | 'and';
amount_from?: string;
amount_to?: string;
Expand Down Expand Up @@ -39,8 +41,8 @@ export type AdvancedFilterResponseItem = {
}

export type AdvancedFiltersSearchParams = {
methods: Array<AdvancedFilterMethodInfo>;
tokens: Array<TokenInfo>;
methods: Record<string, AdvancedFilterMethodInfo>;
tokens: Record<string, TokenInfo>;
}

export type AdvancedFilterResponse = {
Expand All @@ -59,5 +61,5 @@ export type AdvancedFilterMethodsResponse = Array<AdvancedFilterMethodInfo>;

export type AdvancedFilterMethodInfo = {
method_id: string;
name: string;
name?: string;
}
101 changes: 50 additions & 51 deletions ui/advancedFilter/ColumnFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
IconButton,
chakra,
Flex,
Text,
Expand All @@ -13,63 +7,68 @@ 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 (
<>
<Flex alignItems="center" justifyContent="space-between" mb={ 3 }>
<Text color="text_secondary" fontWeight="600">{ title }</Text>
<Link
onClick={ onReset }
cursor={ isFilled ? 'pointer' : 'unset' }
opacity={ isFilled ? 1 : 0.2 }
_hover={{
color: isFilled ? 'link_hovered' : 'none',
}}
>
Reset
</Link>
</Flex>
{ children }
<Button isDisabled={ !isFilled } mt={ 4 } onClick={ onFilterClick } w="fit-content">
Filter
</Button>
</>
);
};

const ColumnFilter = ({ columnName, isActive, className, isLoading, ...props }: Props) => {
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<IconButton
onClick={ onToggle }
aria-label={ `filter by ${ columnName }` }
variant="ghost"
w="30px"
h="30px"
icon={ <IconSvg name="filter" w="20px" h="20px"/> }
isActive={ isActive }
/>
</PopoverTrigger>
<PopoverContent className={ className }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
<chakra.form
noValidate
onSubmit={ onFilter }
textAlign="start"
>
<Flex alignItems="center" justifyContent="space-between" mb={ 3 }>
<Text color="text_secondary" fontWeight="600">{ title }</Text>
<Link
onClick={ onReset }
cursor={ isFilled ? 'pointer' : 'unset' }
opacity={ isFilled ? 1 : 0.2 }
_hover={{
color: isFilled ? 'link_hovered' : 'none',
}}
>
Reset
</Link>
</Flex>
{ children }
<Button type="submit" isDisabled={ !isFilled } mt={ 4 }>
Filter
</Button>
</chakra.form>
</PopoverBody>
</PopoverContent>
</Popover>
<ColumnFilterWrapper
isActive={ isActive }
columnName={ columnName }
className={ className }
isLoading={ isLoading }
>
<ColumnFilterContent { ...props }/>
</ColumnFilterWrapper>
);
};

Expand Down
57 changes: 57 additions & 0 deletions ui/advancedFilter/ColumnFilterWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ReactNode>;
};

const modifiedChildren = React.cloneElement(
child,
{ onClose },
);

return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy lazyBehavior="unmount">
<PopoverTrigger>
<IconButton
onClick={ onToggle }
aria-label={ `filter by ${ columnName }` }
variant="ghost"
w="30px"
h="30px"
icon={ <IconSvg name="filter" w="20px" h="20px"/> }
isActive={ isActive }
isDisabled={ isLoading }
/>
</PopoverTrigger>
<PopoverContent className={ className }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
{ modifiedChildren }
</PopoverBody>
</PopoverContent>
</Popover>
);
};

export default chakra(ColumnFilterWrapper);
50 changes: 36 additions & 14 deletions ui/advancedFilter/FilterByColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 <TypeFilter { ...commonProps } value={ filters.tx_types }/>;
case 'method': {
const value = filters.methods?.map(m => searchParams?.methods[m] || { method_id: m });
return <MethodFilter { ...commonProps } value={ value }/>;
}
case 'method':
// fix value
return <MethodFilter { ...commonProps } value={ searchParams?.methods }/>;
case 'age':
return <AgeFilter { ...commonProps } value={{ age: filters.age, from: filters.age_from, to: filters.age_to }}/>;
// case 'from':
// return <AddressEntity address={ item.from } truncation="constant"/>;
// case 'to':
// return <AddressEntity address={ item.to } truncation="constant"/>;
case 'or_and':
return <AddressRelationFilter { ...commonProps } value={ filters.address_relation }/>;
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 <AddressFilter { ...commonProps } type="from" value={ value }/>;

}
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 <AddressFilter { ...commonProps } type="to" value={ value }/>;
}
case 'amount':
// fix types
return <AmountFilter { ...commonProps } value={{ from: filters.amount_from, to: filters.amount_to }}/>;
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 <AssetFilter { ...commonProps } value={ value }/>;
}
default:
Expand Down
35 changes: 23 additions & 12 deletions ui/advancedFilter/ItemByColumn.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <TxEntity truncation="constant_long" hash={ item.hash }/>;
return <TxEntity truncation="constant_long" hash={ item.hash } isLoading={ isLoading }/>;
case 'type': {
const type = ADVANCED_FILTER_TYPES.find(t => t.id === item.type);
if (!type) {
return null;
}
return <Tag>{ type.name }</Tag>;
return <Tag isLoading={ isLoading }>{ type.name }</Tag>;
}
case 'method':
return item.method ? <Tag>{ item.method }</Tag> : null;
return item.method ? <Tag isLoading={ isLoading }>{ item.method }</Tag> : null;
case 'age':
return <Text>{ dayjs(item.timestamp).fromNow() }</Text>;
return <Skeleton isLoaded={ !isLoading }>{ dayjs(item.timestamp).fromNow() }</Skeleton>;
case 'from':
return <AddressEntity address={ item.from } truncation="constant"/>;
return <AddressEntity address={ item.from } truncation="constant" isLoading={ isLoading }/>;
case 'to':
return <AddressEntity address={ item.to } truncation="constant"/>;
return <AddressEntity address={ item.to } truncation="constant" isLoading={ isLoading }/>;
case 'amount': {
if (item.total) {
return <Text>{ getCurrencyValue({ value: item.total?.value, decimals: item.total.decimals, accuracy: 8 }).valueStr }</Text>;
return (
<Skeleton isLoaded={ !isLoading }>
{ getCurrencyValue({ value: item.total?.value, decimals: item.total.decimals, accuracy: 8 }).valueStr }
</Skeleton>
);
}
if (item.value) {
return <Text>{ getCurrencyValue({ value: item.value, decimals: config.chain.currency.decimals.toString(), accuracy: 8 }).valueStr }</Text>;
return (
<Skeleton isLoaded={ !isLoading }>
{ getCurrencyValue({ value: item.value, decimals: config.chain.currency.decimals.toString(), accuracy: 8 }).valueStr }
</Skeleton>
);
}
return null;
}
case 'asset':
return item.token ? <TokenEntity token={ item.token }/> : <Text>{ `${ config.chain.currency.name } (${ config.chain.currency.symbol })` }</Text>;
return item.token ?
<TokenEntity token={ item.token } isLoading={ isLoading }/> :
<Skeleton isLoaded={ !isLoading }>{ `${ config.chain.currency.name } (${ config.chain.currency.symbol })` }</Skeleton>;
case 'fee':
return <Text>{ item.fee ? getCurrencyValue({ value: item.fee, accuracy: 8 }).valueStr : '-' }</Text>;
return <Skeleton isLoaded={ !isLoading }>{ item.fee ? getCurrencyValue({ value: item.fee, accuracy: 8 }).valueStr : '-' }</Skeleton>;
default:
return null;
}
Expand Down
Loading

0 comments on commit ecf11ae

Please sign in to comment.