Commit a651daec authored by tom's avatar tom

display table of public tags

parent b96198c8
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/public_tags/${ req.query.id }`;
};
const publicTagsHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default publicTagsHandler;
import type { PublicTags } from 'types/api/account';
import handler from 'lib/api/handler';
const publicKeysHandler = handler<PublicTags>(() => '/account/v1/user/public_tags', [ 'GET', 'POST' ]);
export default publicKeysHandler;
......@@ -71,3 +71,21 @@ export interface WatchlistAddressNew {
}
export type WatchlistAddresses = Array<WatchlistAddress>
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
is_owner: boolean;
id: number;
full_name: string;
email: string;
company: string;
addresses: string; // address_1;<address_2;address_3 etc.
additional_comment: string;
}
export type PublicTagNew = Omit<PublicTag, 'addresses' | 'id'> & {
addresses_array: Array<string>;
}
export type PublicTags = Array<PublicTag>;
......@@ -54,7 +54,7 @@ const ApiKeysPage: React.FC = () => {
return (
<>
<SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
<Skeleton height="48px" width="156px" marginTop={ 8 }/>
</>
);
}
......
......@@ -5,7 +5,8 @@ import {
import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll';
import type { TPublicTagItem } from 'data/publicTags';
import type { PublicTag } from 'types/api/account';
import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
......@@ -20,9 +21,9 @@ const toastDescriptions = {
removed: 'Tags have been removed.',
} as Record<TToastAction, string>;
const PublicTags: React.FC = () => {
const PublicTagsComponent: React.FC = () => {
const [ screen, setScreen ] = useState<TScreen>('data');
const [ formData, setFormData ] = useState<TPublicTagItem>();
const [ formData, setFormData ] = useState<PublicTag>();
const toast = useToast();
......@@ -39,7 +40,7 @@ const PublicTags: React.FC = () => {
});
}, [ toast ]);
const changeToFormScreen = useCallback((data?: TPublicTagItem) => {
const changeToFormScreen = useCallback((data?: PublicTag) => {
setFormData(data);
setScreen('form');
animateScroll.scrollToTop({
......@@ -82,4 +83,4 @@ const PublicTags: React.FC = () => {
);
};
export default PublicTags;
export default PublicTagsComponent;
......@@ -2,22 +2,26 @@ import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react';
import type { TPublicTag } from 'data/publicTags';
import type { PublicTag } from 'types/api/account';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
isOpen: boolean;
onClose: () => void;
tags: Array<TPublicTag>;
data: PublicTag;
onDeleteSuccess: () => void;
}
const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onDeleteSuccess }) => {
const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDeleteSuccess }) => {
const tags = data.tags.split(';');
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', tags);
console.log('delete', data);
onDeleteSuccess();
}, [ tags, onDeleteSuccess ]);
}, [ data, onDeleteSuccess ]);
const [ reason, setReason ] = useState<string>('');
......@@ -31,7 +35,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
text = (
<>
<Text display="flex">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0].name }" ` }</Text>
<Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0] }" ` }</Text>
<Text>will be removed.</Text>
</>
);
......@@ -40,15 +44,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => {
if (index < tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }"` }</Text>);
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }"` }</Text>);
tagsText.push(',');
}
if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }" ` }</Text>);
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }" ` }</Text>);
tagsText.push('and');
}
if (index === tags.length - 1) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }" ` }</Text>);
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }" ` }</Text>);
}
});
text = (
......@@ -85,4 +89,4 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
);
};
export default DeletePublicTagModal;
export default React.memo(DeletePublicTagModal);
......@@ -8,14 +8,14 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type { TPublicTagItem, TPublicTags } from 'data/publicTags';
import type { PublicTags, PublicTag } from 'types/api/account';
import PublicTagTableItem from './PublicTagTableItem';
interface Props {
data: TPublicTags;
onEditClick: (data: TPublicTagItem) => void;
onDeleteClick: (data: TPublicTagItem) => void;
data: PublicTags;
onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void;
}
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
......@@ -26,12 +26,12 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
<Tr>
<Th width="60%">Smart contract / Address (0x...)</Th>
<Th width="40%">Public tag</Th>
<Th width="200px">Submission date</Th>
<Th width="200px">Request status</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: TPublicTagItem) => (
{ data.map((item) => (
<PublicTagTableItem
item={ item }
key={ item.id }
......
import {
Box,
Tag,
Text,
Tr,
Td,
HStack,
VStack,
useColorModeValue,
Text,
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { TPublicTagItem, TPublicTagAddress, TPublicTag } from 'data/publicTags';
import type { PublicTag } from 'types/api/account';
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import DeleteButton from 'ui/shared/DeleteButton';
......@@ -18,9 +18,9 @@ import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props {
item: TPublicTagItem;
onEditClick: (data: TPublicTagItem) => void;
onDeleteClick: (data: TPublicTagItem) => void;
item: PublicTag;
onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void;
}
const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
......@@ -32,19 +32,18 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
return (
<Tr alignItems="top" key={ item.id }>
<Td>
<VStack spacing={ 4 } alignItems="unset">
{ item.addresses.map((adr: TPublicTagAddress) => {
{ item.addresses.split(';').map((address) => {
return (
<HStack spacing={ 4 } key={ adr.address } overflow="hidden" alignItems="start">
<AddressIcon address={ adr.address }/>
<HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start">
<AddressIcon address={ address }/>
<Box overflow="hidden">
<AddressLinkWithTooltip address={ adr.address }/>
{ adr.addressName && <Text fontSize="sm" color={ secondaryColor } mt={ 0.5 }>{ adr.addressName }</Text> }
<AddressLinkWithTooltip address={ address }/>
{ /* todo_tom add address name */ }
<Text fontSize="sm" variant="secondary" mt={ 0.5 }>Address Name</Text>
</Box>
</HStack>
);
......@@ -53,19 +52,23 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Td>
<Td>
<VStack spacing={ 2 } alignItems="baseline">
{ item.tags.map((tag: TPublicTag) => {
{ item.tags.split(';').map((tag) => {
return (
<TruncatedTextTooltip label={ tag.name } key={ tag.name }>
<Tag color={ tag.colorHex || 'gray.600' } background={ tag.backgroundHex || 'gray.200' } lineHeight="24px">
{ tag.name }
<TruncatedTextTooltip label={ tag } key={ tag }>
<Tag variant="gray" lineHeight="24px">
{ tag }
</Tag>
</TruncatedTextTooltip>
);
}) }
</VStack>
</Td>
{ /* todo_tom update tag date and status */ }
<Td>
<Text fontSize="sm" color={ secondaryColor }>{ item.date }</Text>
<VStack alignItems="flex-start">
<Text fontSize="sm" color="green.500" fontWeight="500">Approved</Text>
<Text fontSize="sm" variant="secondary">Jun 10, 2022</Text>
</VStack>
</Td>
<Td>
<HStack spacing={ 6 }>
......@@ -77,4 +80,4 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
);
};
export default PublicTagTableItem;
export default React.memo(PublicTagTableItem);
import { Box, Text, Button, useDisclosure } from '@chakra-ui/react';
import { Box, Text, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TPublicTagItem, TPublicTag } from 'data/publicTags';
import { publicTags } from 'data/publicTags';
import type { PublicTags, PublicTag } from 'types/api/account';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal';
import PublicTagTable from './PublicTagTable/PublicTagTable';
type Props = {
changeToFormScreen: (data?: TPublicTagItem) => void;
changeToFormScreen: (data?: PublicTag) => void;
onTagDelete: () => void;
}
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<Array<TPublicTag>>([]);
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => {
const response = await fetch('/api/account/public-tags');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
const onDeleteModalClose = useCallback(() => {
setDeleteModalData([]);
setDeleteModalData(undefined);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
......@@ -25,24 +35,28 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
changeToFormScreen();
}, [ changeToFormScreen ]);
const onItemEditClick = useCallback((item: TPublicTagItem) => {
const onItemEditClick = useCallback((item: PublicTag) => {
changeToFormScreen(item);
}, [ changeToFormScreen ]);
const onItemDeleteClick = useCallback((item: TPublicTagItem) => {
setDeleteModalData(item.tags);
const onItemDeleteClick = useCallback((item: PublicTag) => {
setDeleteModalData(item);
deleteModalProps.onOpen();
}, [ deleteModalProps ]);
const content = (() => {
if (isLoading || isError) {
return (
<>
<Text marginBottom={ 12 }>
You can request a public category tag which is displayed to all Blockscout users.
Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag.
Clicking a tag opens a page with related information and helps provide context and data organization.
Requests are sent to a moderator for review and approval. This process can take several days.
</Text>
<PublicTagTable data={ publicTags } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/>
<SkeletonTable columns={ [ '60%', '40%', '200px', '108px' ] }/>
<Skeleton height="48px" width="270px" marginTop={ 8 }/>
</>
);
}
return (
<>
{ data.length > 0 && <PublicTagTable data={ data } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> }
<Box marginTop={ 8 }>
<Button
variant="primary"
......@@ -52,12 +66,27 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
Request to add public tag
</Button>
</Box>
</>
);
})();
return (
<>
<Text marginBottom={ 12 }>
You can request a public category tag which is displayed to all Blockscout users.
Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag.
Clicking a tag opens a page with related information and helps provide context and data organization.
Requests are sent to a moderator for review and approval. This process can take several days.
</Text>
{ content }
{ deleteModalData && (
<DeletePublicTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
tags={ deleteModalData }
data={ deleteModalData }
onDeleteSuccess={ onTagDelete }
/>
) }
</>
);
};
......
......@@ -10,7 +10,7 @@ import React, { useCallback } from 'react';
import type { Path } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form';
import type { TPublicTagItem, TPublicTag, TPublicTagAddress } from 'data/publicTags';
import type { PublicTag } from 'types/api/account';
import PublicTagFormAction from './PublicTagFormAction';
import PublicTagFormAddressInput from './PublicTagFormAddressInput';
......@@ -19,7 +19,7 @@ import PublicTagsFormInput from './PublicTagsFormInput';
type Props = {
changeToDataScreen: (success?: boolean) => void;
data?: TPublicTagItem;
data?: PublicTag;
}
export type Inputs = {
......@@ -50,14 +50,14 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 170;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({
defaultValues: {
userName: data?.userName,
userEmail: data?.userEmail,
companyName: data?.companyName,
companyUrl: data?.companyUrl,
tag: data?.tags.map((tag: TPublicTag) => tag.name).join('; '),
addresses: data?.addresses.map((adr: TPublicTagAddress, index: number) => ({ name: `address.${ index }.address`, address: adr.address })) ||
userName: data?.full_name,
userEmail: data?.email,
companyName: data?.company,
companyUrl: data?.website,
tag: data?.tags.split(';').map((tag) => tag).join('; '),
addresses: data?.addresses.split(';').map((address, index: number) => ({ name: `address.${ index }.address`, address })) ||
[ { name: 'address.0.address', address: '' } ],
comment: data?.comment,
comment: data?.additional_comment,
},
});
......@@ -135,4 +135,4 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
);
};
export default PublicTagsForm;
export default React.memo(PublicTagsForm);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment