Skip to content

Commit

Permalink
watchlist and private tags pagination (#1333)
Browse files Browse the repository at this point in the history
Co-authored-by: isstuev <[email protected]>
  • Loading branch information
isstuev and ArminaAiren authored Nov 8, 2023
1 parent 15cfad3 commit a8c723c
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 69 deletions.
24 changes: 14 additions & 10 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import type {
UserInfo,
CustomAbis,
PublicTags,
AddressTags,
TransactionTags,
ApiKeys,
WatchlistAddress,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
TokenInfoApplications,
WatchlistResponse,
TransactionTagsResponse,
AddressTagsResponse,
} from 'types/api/account';
import type {
Address,
Expand Down Expand Up @@ -90,20 +90,23 @@ export const RESOURCES = {
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
path: '/api/account/v2/user/watchlist/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
path: '/api/account/v2/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
Expand Down Expand Up @@ -579,7 +582,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx';

export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;

Expand All @@ -588,10 +592,10 @@ export type ResourcePayload<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications :
Expand Down
24 changes: 24 additions & 0 deletions types/api/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export interface AddressTag {

export type AddressTags = Array<AddressTag>

export type AddressTagsResponse = {
items: AddressTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}

export interface ApiKey {
api_key: string;
name: string;
Expand Down Expand Up @@ -48,6 +56,14 @@ export interface TransactionTag {

export type TransactionTags = Array<TransactionTag>

export type TransactionTagsResponse = {
items: TransactionTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}

export type Transactions = Array<Transaction>

export interface UserInfo {
Expand Down Expand Up @@ -78,6 +94,14 @@ export interface WatchlistAddressNew {

export type WatchlistAddresses = Array<WatchlistAddress>

export type WatchlistResponse = {
items: WatchlistAddresses;
next_page_params: {
id: number;
items_count: number;
} | null;
}

export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
Expand Down
48 changes: 32 additions & 16 deletions ui/pages/Watchlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';

import type { WatchlistAddress } from 'types/api/account';
import type { WatchlistAddress, WatchlistResponse } from 'types/api/account';

import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { WATCH_LIST_ITEM_WITH_TOKEN_INFO } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';

const WatchList: React.FC = () => {
const { data, isPlaceholderData, isError } = useApiQuery('watchlist', {
queryOptions: {
placeholderData: Array(3).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO),

const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'watchlist',
options: {
placeholderData: { items: Array(5).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO), next_page_params: null },
},
});
const queryClient = useQueryClient();
Expand Down Expand Up @@ -58,9 +63,11 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]);

const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ resourceKey('watchlist') ], (prevData: Array<WatchlistAddress> | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
queryClient.setQueryData(getResourceKey('watchlist'), (prevData: WatchlistResponse | undefined) => {
const newItems = prevData?.items.filter((item: WatchlistAddress) => item.id !== deleteModalData?.id);
return { ...prevData, items: newItems };
},
);
}, [ deleteModalData?.id, queryClient ]);

const description = (
Expand All @@ -69,15 +76,17 @@ const WatchList: React.FC = () => {
</AccountPageDescription>
);

if (isError) {
return <DataFetchAlert/>;
}

const content = (() => {
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;

const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
{ data?.items.map((item, index) => (
<WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item }
Expand All @@ -89,10 +98,11 @@ const WatchList: React.FC = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable
data={ data }
data={ data?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
Expand All @@ -101,7 +111,13 @@ const WatchList: React.FC = () => {
return (
<>
{ description }
{ Boolean(data?.length) && list }
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
Expand Down
10 changes: 6 additions & 4 deletions ui/privateTags/AddressTagTable/AddressTagTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Table,
Thead,
Tbody,
Tr,
Th,
Expand All @@ -9,25 +8,28 @@ import React from 'react';

import type { AddressTags, AddressTag } from 'types/api/account';

import TheadSticky from 'ui/shared/TheadSticky';

import AddressTagTableItem from './AddressTagTableItem';

interface Props {
data?: AddressTags;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
top: number;
}

const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="116px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
Expand Down
15 changes: 9 additions & 6 deletions ui/privateTags/DeletePrivateTagModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';

import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import type { AddressTag, TransactionTag, AddressTagsResponse, TransactionTagsResponse } from 'types/api/account';

import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import DeleteModal from 'ui/shared/DeleteModal';

type Props = {
Expand All @@ -32,12 +32,15 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })

const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ resourceKey('private_tags_address') ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_address'), (prevData: AddressTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: AddressTag) => item.id !== id);
return { ...prevData, items: newItems };

});
} else {
queryClient.setQueryData([ resourceKey('private_tags_tx') ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_tx'), (prevData: TransactionTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: TransactionTag) => item.id !== id);
return { ...prevData, items: newItems };
});
}
}, [ type, id, queryClient ]);
Expand Down
36 changes: 24 additions & 12 deletions ui/privateTags/PrivateAddressTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ import React, { useCallback, useState } from 'react';

import type { AddressTag } from 'types/api/account';

import useApiQuery from 'lib/api/useApiQuery';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';

import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';

const PrivateAddressTags = () => {
const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
const { data: addressTagsData, isError, isPlaceholderData, refetch, pagination } = useQueryWithPages({
resourceName: 'private_tags_address',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
placeholderData: { items: Array(5).fill(PRIVATE_TAG_ADDRESS), next_page_params: null },
},
});

Expand Down Expand Up @@ -52,14 +55,10 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);

if (isError) {
return <DataFetchAlert/>;
}

const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ addressTagsData?.map((item: AddressTag, index: number) => (
{ addressTagsData?.items.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
Expand All @@ -72,21 +71,34 @@ const PrivateAddressTags = () => {
<Box display={{ base: 'none', lg: 'block' }}>
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
data={ addressTagsData?.items }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);

const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;

return (
<>
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ addressTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
Expand Down
Loading

0 comments on commit a8c723c

Please sign in to comment.