Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"postinstall": "chakra typegen ./toolkit/theme/theme.ts"
},
"dependencies": {
"@arkiv-network/sdk": "^0.3.1",
"@arkiv-network/sdk": "^0.5.3",
"@blockscout/bens-types": "1.4.1",
"@blockscout/multichain-aggregator-types": "1.6.0-alpha.0",
"@blockscout/points-types": "1.3.0-alpha.2",
Expand Down
10 changes: 5 additions & 5 deletions playwright/fixtures/mockArkiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import type { TestFixture, Page } from '@playwright/test';

interface MockArkivConfig {
isConnected?: boolean;
createEntitiesResponse?: Array<{ entityKey: string }>;
updateEntitiesResponse?: Array<{ entityKey: string }>;
queryResponse?: Array<{ key: string; storageValue: Uint8Array }>;
createEntityResponse?: Array<{ entityKey: string }>;
updateEntityResponse?: Array<{ entityKey: string }>;
queryResponse?: { entities: Array<{ key: string }>; cursor?: string };
}

export type MockArkivFixture = (config?: MockArkivConfig) => Promise<void>;

const fixture: TestFixture<MockArkivFixture, { page: Page }> = async({ page }, use) => {
await use(async(config = {}) => {
const defaultEntityKey = '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995';
const golemBaseConfig = {
const golemBaseConfig: MockArkivConfig = {
isConnected: false,
createEntityResponse: [ { entityKey: defaultEntityKey } ],
updateEntityResponse: [ { entityKey: defaultEntityKey } ],
queryEntitiesResponse: [],
queryResponse: { entities: [] },
...config,
};

Expand Down
4 changes: 2 additions & 2 deletions ui/entity/fields/EntityFieldAnnotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const EntityFieldAnnotations = ({ variant, hint }: Props) => {
inputProps: { type: 'number' },
rules: {
min: {
value: 1,
message: 'Must be at least 1',
value: 0,
message: 'Must be at least 0',
},
max: {
value: Number.MAX_SAFE_INTEGER,
Expand Down
12 changes: 6 additions & 6 deletions ui/entity/utils/useQueryEntities.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import type { Entity } from '@arkiv-network/sdk';
import type { QueryOptions, QueryReturnType } from '@arkiv-network/sdk';
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

import { createPublicClient } from 'lib/arkiv/useArkivClient';

export default function useQueryEntities(
searchTerm: string,
options?: Omit<
UseQueryOptions<Array<Entity>, Error, Array<Entity>>,
{ searchOptions, ...options }: { searchOptions?: QueryOptions } & Omit<
UseQueryOptions<QueryReturnType, Error, QueryReturnType>,
'queryKey' | 'queryFn'
>,
> = {},
) {
return useQuery({
queryKey: [ 'golemBase', 'queryEntities', { searchTerm } ],
queryKey: [ 'golemBase', 'queryEntities', searchTerm, searchOptions ],
queryFn: async() => {
const client = createPublicClient();
return client.query(searchTerm);
return client.query(searchTerm, searchOptions);
},
enabled: options?.enabled !== false && Boolean(searchTerm?.trim()),
...options,
Expand Down
27 changes: 4 additions & 23 deletions ui/pages/EntitySearch.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,12 @@ test('with search results +@dark-mode +@mobile', async({ render, mockArkiv }) =>
};

const mockEntities = [
{
key: '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995',
storageValue: new Uint8Array([
0x7b, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x65, 0x73, 0x74, 0x20,
0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x20, 0x31, 0x22, 0x7d,
]),
},
{
key: '0x742d35cc6bc59fb56d41229f1e5e0d5c3f2cf9b8e1a9c2d3f4e5f6a7b8c9d0e1f2',
storageValue: new Uint8Array([
0x7b, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x53, 0x61, 0x6d, 0x70, 0x6c,
0x65, 0x20, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x7d,
]),
},
{
key: '0x8ba1f109551bd432803012645hac136c0b1b6b7c2d8e3f9a1b4c5d6e7f8a9b0c',
storageValue: new Uint8Array([
0x7b, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a,
0x20, 0x22, 0x41, 0x6e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x20, 0x65, 0x6e, 0x74, 0x69, 0x74,
0x79, 0x22, 0x7d,
]),
},
{ key: '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995' },
{ key: '0x742d35cc6bc59fb56d41229f1e5e0d5c3f2cf9b8e1a9c2d3f4e5f6a7b8c9d0e1f2' },
{ key: '0x8ba1f109551bd432803012645hac136c0b1b6b7c2d8e3f9a1b4c5d6e7f8a9b0c' },
];

await mockArkiv({ queryResponse: mockEntities });
await mockArkiv({ queryResponse: { entities: mockEntities } });

const component = await render(<EntitySearch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
Expand Down
132 changes: 113 additions & 19 deletions ui/pages/EntitySearch.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Box, chakra } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';

import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENTITY_QUERY_ITEM } from 'stubs/entity';
import { Button } from 'toolkit/chakra/button';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import useQueryEntities from 'ui/entity/utils/useQueryEntities';
import QueryBuilder from 'ui/queryBuilder/QueryBuilder';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import * as Layout from 'ui/shared/layout/components';
import PageTitle from 'ui/shared/Page/PageTitle';
import HeaderAlert from 'ui/snippets/header/HeaderAlert';
Expand All @@ -23,30 +26,88 @@ import HeaderMobile from 'ui/snippets/header/HeaderMobile';
const SearchEntityPageContent = () => {
const router = useRouter();
const searchTerm = getQueryParamString(router.query.q)?.trim() || '';
const cursor = getQueryParamString(router.query.cursor) || undefined;

const enabled = Boolean(searchTerm);

const cursorHistoryRef = React.useRef<Array<string | undefined>>([]);
const prevSearchTermRef = React.useRef<string>('');

React.useEffect(() => {
if (searchTerm !== prevSearchTermRef.current) {
cursorHistoryRef.current = [];
prevSearchTermRef.current = searchTerm;
}
}, [ searchTerm ]);

const { data, isError, isPlaceholderData: isLoading } = useQueryEntities(searchTerm, {
placeholderData: Array(50).fill(ENTITY_QUERY_ITEM),
enabled,
placeholderData: {
entities: Array(50).fill(ENTITY_QUERY_ITEM),
cursor: undefined,
blockNumber: undefined,
},
searchOptions: {
resultsPerPage: 50,
includeData: { payload: false },
cursor,
},
});

const { cutRef, renderedItemsNum } = useLazyRenderedList(data || [], !isLoading, 50);

const handleSubmit = React.useCallback((value: string) => {
if (value) {
router.push({ pathname: '/entity/search', query: { q: value } }, undefined, { shallow: true });
}
}, [ router ]);

const handleFirstClick = React.useCallback(() => {
const query = { ...router.query };
delete query.cursor;
cursorHistoryRef.current = [];
router.push({ pathname: '/entity/search', query }, undefined, { shallow: true });
}, [ router ]);

const handlePrevClick = React.useCallback(() => {
const prevQuery = { ...router.query };

if (cursorHistoryRef.current.length === 0) {
delete prevQuery.cursor;
} else {
const prevCursors = [ ...cursorHistoryRef.current ];
const prevCursor = prevCursors.pop();
cursorHistoryRef.current = prevCursors;

if (prevCursor) {
prevQuery.cursor = prevCursor;
} else {
delete prevQuery.cursor;
}
}

router.push({ pathname: '/entity/search', query: prevQuery }, undefined, { shallow: true });
}, [ router ]);

const handleNextClick = React.useCallback(() => {
if (!data?.cursor) {
return;
}

cursorHistoryRef.current = [ ...cursorHistoryRef.current, cursor ];

router.push(
{ pathname: '/entity/search', query: { ...router.query, cursor: data.cursor } },
undefined,
{ shallow: true },
);
}, [ data, cursor, router ]);

const displayedItems = React.useMemo(() => {
if (!data) return [];

return data.slice(0, renderedItemsNum).map((item) => ({
return data.entities.map((item) => ({
type: 'golembase_entity' as const,
golembase_entity: item.key,
}));
}, [ data, renderedItemsNum ]);
}, [ data ]);

const content = (() => {
if (isError) {
Expand All @@ -72,7 +133,6 @@ const SearchEntityPageContent = () => {
isLoading={ isLoading }
/>
)) }
<Box ref={ cutRef } h={ 0 }/>
</Box>
<Box hideBelow="lg">
<TableRoot fontWeight={ 500 }>
Expand All @@ -95,25 +155,59 @@ const SearchEntityPageContent = () => {
)) }
</TableBody>
</TableRoot>
<Box ref={ cutRef } h={ 0 }/>
</Box>
</>
);
})();

const bar = (() => {
const pagination = (() => {
if (isError || !enabled) {
return null;
}

const resultsCount = data?.length ?? 0;
const hasPagination = cursor || data?.cursor;

return isLoading ? (
<Skeleton loading h={ 6 } w="280px" borderRadius="full" mb={ 6 }/>
) : (
<Box mb={ 6 } lineHeight="32px">
<span>Found <chakra.span fontWeight={ 700 }>{ resultsCount }</chakra.span> matching entit{ resultsCount === 1 ? 'y' : 'ies' }</span>
</Box>
if (!hasPagination) {
return null;
}

const showSkeleton = !cursor && !data?.cursor && isLoading;

return (
<ActionBar mt={{ base: 0, lg: -6 }} alignItems="center">
<Flex as="nav" alignItems="center" ml="auto" my={ 1 } gap={ 2 }>
<Skeleton loading={ showSkeleton }>
<Button
variant="pagination"
size="sm"
onClick={ handleFirstClick }
disabled={ !cursor || isLoading }
>
First
</Button>
</Skeleton>
<IconButton
aria-label="Prev page"
variant="pagination"
boxSize={ 8 }
onClick={ handlePrevClick }
disabled={ !cursor || isLoading }
loadingSkeleton={ showSkeleton }
>
<IconSvg name="arrows/east-mini" boxSize={ 5 }/>
</IconButton>
<IconButton
aria-label="Next page"
variant="pagination"
boxSize={ 8 }
onClick={ handleNextClick }
disabled={ !data?.cursor || isLoading }
loadingSkeleton={ showSkeleton }
>
<IconSvg name="arrows/east-mini" boxSize={ 5 } transform="rotate(180deg)"/>
</IconButton>
</Flex>
</ActionBar>
);
})();

Expand All @@ -127,7 +221,7 @@ const SearchEntityPageContent = () => {
isLoading={ isLoading && enabled }
/>
</Box>
{ bar }
{ pagination }
{ content }
</>
);
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.
8 changes: 4 additions & 4 deletions ui/queryBuilder/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ export const OPERATOR_CONFIGS: Array<OperatorConfig> = [
name: '<',
label: '<',
description: 'Less than operator',
supportedFields: [ 'numeric' ],
supportedFields: [ 'string', 'numeric' ],
},
{
name: '<=',
label: '<=',
description: 'Less than or equal operator',
supportedFields: [ 'numeric' ],
supportedFields: [ 'string', 'numeric' ],
},
{
name: '>',
label: '>',
description: 'Greater than operator',
supportedFields: [ 'numeric' ],
supportedFields: [ 'string', 'numeric' ],
},
{
name: '>=',
label: '>=',
description: 'Greater than or equal operator',
supportedFields: [ 'numeric' ],
supportedFields: [ 'string', 'numeric' ],
},
{
name: '~',
Expand Down
4 changes: 1 addition & 3 deletions ui/snippets/searchBar/SearchBar.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ test('search by entity query +@mobile', async({ render, page, mockApiResponse, m
const apiUrl = await mockApiResponse('general:quick_search', [], { queryParams: { q: 'test' } });
await mockArkiv({
isConnected: true,
queryResponse: [
{ key: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', storageValue: new Uint8Array([ 1, 2, 3 ]) },
],
queryResponse: { entities: [ { key: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' } ] },
});
await render(<SearchBar/>);
await page.getByPlaceholder(/search/i).fill('test');
Expand Down
7 changes: 4 additions & 3 deletions ui/snippets/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ const SearchBar = ({ isHomepage }: Props) => {
event.preventDefault();
if (searchTerm && searchTerm === debouncedSearchTerm && entitiesQuery.isFetched) {
const resultRoute: Route = (() => {
if (entitiesQuery.data?.length === 1) {
const entityKey = entitiesQuery.data[0].key;
const entities = entitiesQuery.data?.entities ?? [];
if (entities.length === 1) {
const entityKey = entities[0].key;
return { pathname: '/entity/[key]', query: { key: entityKey } };
}

if (entitiesQuery.data && entitiesQuery.data.length > 1) {
if (entities.length > 1) {
return { pathname: '/entity/search', query: { q: searchTerm } };
}

Expand Down
Loading
Loading