Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support metadata_tag type in search #2559

Merged
merged 2 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions mocks/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SearchResultUserOp,
SearchResultBlob,
SearchResultDomain,
SearchResultMetadataTag,
} from 'types/api/search';

export const token1: SearchResultToken = {
Expand Down Expand Up @@ -147,6 +148,42 @@ export const domain1: SearchResultDomain = {
url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
};

export const metatag1: SearchResultMetadataTag = {
...address1,
type: 'metadata_tag',
metadata: {
name: 'utko',
slug: 'utko',
meta: {},
tagType: 'name',
ordinal: 1,
},
};

export const metatag2: SearchResultMetadataTag = {
...address2,
type: 'metadata_tag',
metadata: {
name: 'utko',
slug: 'utko',
meta: {},
tagType: 'name',
ordinal: 1,
},
};

export const metatag3: SearchResultMetadataTag = {
...contract2,
type: 'metadata_tag',
metadata: {
name: 'super utko',
slug: 'super-utko',
meta: {},
tagType: 'protocol',
ordinal: 1,
},
};

export const baseResponse: SearchResult = {
items: [
token1,
Expand All @@ -157,6 +194,8 @@ export const baseResponse: SearchResult = {
tx1,
blob1,
domain1,
metatag1,

],
next_page_params: null,
};
63 changes: 46 additions & 17 deletions types/api/search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import type * as bens from '@blockscout/bens-types';
import type { TokenType } from 'types/api/token';

export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract';
import type { AddressMetadataTagApi } from './addressMetadata';

export const SEARCH_RESULT_TYPES = {
token: 'token',
address: 'address',
block: 'block',
transaction: 'transaction',
contract: 'contract',
ens_domain: 'ens_domain',
label: 'label',
user_operation: 'user_operation',
blob: 'blob',
metadata_tag: 'metadata_tag',
} as const;

export type SearchResultType = typeof SEARCH_RESULT_TYPES[keyof typeof SEARCH_RESULT_TYPES];

export interface SearchResultToken {
type: 'token';
Expand All @@ -20,29 +35,35 @@ export interface SearchResultToken {
certified?: boolean;
}

export interface SearchResultAddressOrContract {
type: 'address' | 'contract';
type SearchResultEnsInfo = {
address_hash: string;
expiry_date?: string;
name: string;
names_count: number;
} | null;

interface SearchResultAddressData {
name: string | null;
address: string;
is_smart_contract_verified: boolean;
certified?: true;
filecoin_robust_address?: string | null;
url?: string; // not used by the frontend, we build the url ourselves
ens_info?: {
address_hash: string;
expiry_date?: string;
name: string;
names_count: number;
};
}

export interface SearchResultDomain {
export interface SearchResultAddressOrContract extends SearchResultAddressData {
type: 'address' | 'contract';
ens_info?: SearchResultEnsInfo;
}

export interface SearchResultMetadataTag extends SearchResultAddressData {
type: 'metadata_tag';
ens_info?: SearchResultEnsInfo;
metadata: AddressMetadataTagApi;
}

export interface SearchResultDomain extends SearchResultAddressData {
type: 'ens_domain';
name: string | null;
address: string;
filecoin_robust_address?: string | null;
is_smart_contract_verified: boolean;
url?: string; // not used by the frontend, we build the url ourselves
ens_info: {
address_hash: string;
expiry_date?: string;
Expand Down Expand Up @@ -90,8 +111,16 @@ export interface SearchResultUserOp {
url?: string; // not used by the frontend, we build the url ourselves
}

export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp |
SearchResultBlob | SearchResultDomain;
export type SearchResultItem =
SearchResultToken |
SearchResultAddressOrContract |
SearchResultBlock |
SearchResultTx |
SearchResultLabel |
SearchResultUserOp |
SearchResultBlob |
SearchResultDomain |
SearchResultMetadataTag;

export interface SearchResult {
items: Array<SearchResultItem>;
Expand Down
18 changes: 18 additions & 0 deletions ui/pages/SearchResults.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test.describe('search by name', () => {
searchMock.token2,
searchMock.contract1,
searchMock.address2,
searchMock.metatag1,
searchMock.label1,
],
next_page_params: null,
Expand Down Expand Up @@ -52,6 +53,23 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => {
await expect(component.locator('main')).toHaveScreenshot();
});

test('search by meta tag +@mobile', async({ render, mockApiResponse }) => {
const hooksConfig = {
router: {
query: { q: 'utko' },
},
};
const data = {
items: [ searchMock.metatag1, searchMock.metatag2, searchMock.metatag3 ],
next_page_params: null,
};
await mockApiResponse('search', data, { queryParams: { q: 'utko' } });

const component = await render(<SearchResults/>, { hooksConfig });

await expect(component.locator('main')).toHaveScreenshot();
});

test('search by block number +@mobile', async({ render, mockApiResponse }) => {
const hooksConfig = {
router: {
Expand Down
4 changes: 4 additions & 0 deletions ui/pages/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import type { FormEvent } from 'react';
import React from 'react';

import { SEARCH_RESULT_TYPES } from 'types/api/search';
import type { SearchResultItem } from 'types/client/search';

import config from 'configs/app';
Expand Down Expand Up @@ -95,6 +96,9 @@ const SearchResultsPageContent = () => {

const displayedItems: Array<SearchResultItem | SearchResultAppItem> = React.useMemo(() => {
const apiData = (data?.items || []).filter((item) => {
if (!SEARCH_RESULT_TYPES[item.type]) {
return false;
}
if (!config.features.userOps.isEnabled && item.type === 'user_operation') {
return false;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 29 additions & 15 deletions ui/searchResults/SearchResultListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
Expand Down Expand Up @@ -80,6 +81,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
);
}

case 'metadata_tag':
case 'contract':
case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
Expand Down Expand Up @@ -367,27 +369,39 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
</Text>
);
}
case 'metadata_tag':
case 'contract':
case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const addressName = data.name || data.ens_info?.name;
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';

return addressName ? (
<Flex alignItems="center">
<Text
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
{ data.ens_info && (
data.ens_info.names_count > 1 ?
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> :
<chakra.span color="text_secondary">{ expiresText }</chakra.span>
) }
</Text>
{ data.certified && <ContractCertifiedLabel iconSize={ 4 } boxSize={ 4 } ml={ 1 }/> }
return (addressName || data.type === 'metadata_tag') ? (
<Flex alignItems="center" gap={ 2 } justifyContent="space-between" flexWrap="wrap">
{ addressName && (
<Flex alignItems="center">
<Text
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
{ data.ens_info && (
data.ens_info.names_count > 1 ?
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> :
<chakra.span color="text_secondary">{ expiresText }</chakra.span>
) }
</Text>
{ data.certified && <ContractCertifiedLabel iconSize={ 4 } boxSize={ 4 } ml={ 1 }/> }
</Flex>
) }
{ data.type === 'metadata_tag' && (
// we show regular tag because we don't need all meta info here, but need to highlight search term
<Tag display="flex" alignItems="center">
<EntityTagIcon data={ data.metadata }/>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.metadata.name, searchTerm) }}/>
</Tag>
) }
</Flex>
) :
null;
Expand Down
17 changes: 15 additions & 2 deletions ui/searchResults/SearchResultTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
Expand Down Expand Up @@ -100,6 +101,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
);
}

case 'metadata_tag':
case 'contract':
case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
Expand All @@ -120,7 +122,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P

return (
<>
<Td fontSize="sm" colSpan={ addressName ? 1 : 3 }>
<Td fontSize="sm" colSpan={ (addressName || data.type === 'metadata_tag') ? 1 : 3 } verticalAlign="middle">
<AddressEntity.Container>
<AddressEntity.Icon address={ address }/>
<AddressEntity.Link
Expand All @@ -139,7 +141,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
</AddressEntity.Container>
</Td>
{ addressName && (
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
<Td colSpan={ data.type === 'metadata_tag' ? 1 : 2 } fontSize="sm" verticalAlign="middle">
<Flex alignItems="center">
<Text
overflow="hidden"
Expand All @@ -160,6 +162,17 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
</Flex>
</Td>
) }
{ data.type === 'metadata_tag' && (
<Td colSpan={ addressName ? 1 : 2 } fontSize="sm" verticalAlign="middle">
<Flex justifyContent="flex-end">
{ /* we show regular tag because we don't need all meta info here, but need to highlight search term */ }
<Tag display="flex" alignItems="center">
<EntityTagIcon data={ data.metadata } iconColor="gray.400"/>
<span dangerouslySetInnerHTML={{ __html: highlightText(data.metadata.name, searchTerm) }}/>
</Tag>
</Flex>
</Td>
) }
</>
);
}
Expand Down
23 changes: 3 additions & 20 deletions ui/shared/EntityTags/EntityTag.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ResponsiveValue } from '@chakra-ui/react';
import { chakra, Image, Tag } from '@chakra-ui/react';
import { Tag } from '@chakra-ui/react';
import React from 'react';

import type { EntityTag as TEntityTag } from './types';

import Skeleton from 'ui/shared/chakra/Skeleton';
import IconSvg from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue';

import EntityTagIcon from './EntityTagIcon';
import EntityTagLink from './EntityTagLink';
import EntityTagPopover from './EntityTagPopover';
import { getTagLinkParams } from './utils';
Expand All @@ -26,7 +26,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
}

const hasLink = !noLink && Boolean(getTagLinkParams(data));
const iconColor = data.meta?.textColor ?? 'gray.400';

const name = (() => {
if (data.meta?.warpcastHandle) {
Expand All @@ -36,22 +35,6 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
return data.name;
})();

const icon = (() => {
if (data.meta?.tagIcon) {
return <Image boxSize={ 3 } mr={ 1 } flexShrink={ 0 } src={ data.meta.tagIcon } alt={ `${ data.name } icon` }/>;
}

if (data.tagType === 'name') {
return <IconSvg name="publictags_slim" boxSize={ 3 } mr={ 1 } flexShrink={ 0 } color={ iconColor }/>;
}

if (data.tagType === 'protocol' || data.tagType === 'generic') {
return <chakra.span color={ iconColor } whiteSpace="pre"># </chakra.span>;
}

return null;
})();

return (
<EntityTagPopover data={ data }>
<Tag
Expand All @@ -66,7 +49,7 @@ const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
_hover={ hasLink ? { opacity: 0.76 } : undefined }
>
<EntityTagLink data={ data } noLink={ noLink }>
{ icon }
<EntityTagIcon data={ data } iconColor={ data.meta?.textColor }/>
<TruncatedValue value={ name } tooltipPlacement="top"/>
</EntityTagLink>
</Tag>
Expand Down
Loading
Loading