Commit b007a47e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #93 from blockscout/public-keys-backend

public keys & backend
parents 29b8fd2b 09f3a1d1
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;
...@@ -72,6 +72,24 @@ export interface WatchlistAddressNew { ...@@ -72,6 +72,24 @@ export interface WatchlistAddressNew {
export type WatchlistAddresses = Array<WatchlistAddress> 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>;
export type CustomAbis = Array<CustomAbi> export type CustomAbis = Array<CustomAbi>
export interface CustomAbi { export interface CustomAbi {
......
...@@ -54,7 +54,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -54,7 +54,7 @@ const ApiKeysPage: React.FC = () => {
return ( return (
<> <>
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="48px" width="156px" marginTop={ 8 }/>
</> </>
); );
} }
......
...@@ -5,7 +5,8 @@ import { ...@@ -5,7 +5,8 @@ import {
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll'; 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 PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm'; import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import AccountPageHeader from 'ui/shared/AccountPageHeader'; import AccountPageHeader from 'ui/shared/AccountPageHeader';
...@@ -20,9 +21,9 @@ const toastDescriptions = { ...@@ -20,9 +21,9 @@ const toastDescriptions = {
removed: 'Tags have been removed.', removed: 'Tags have been removed.',
} as Record<TToastAction, string>; } as Record<TToastAction, string>;
const PublicTags: React.FC = () => { const PublicTagsComponent: React.FC = () => {
const [ screen, setScreen ] = useState<TScreen>('data'); const [ screen, setScreen ] = useState<TScreen>('data');
const [ formData, setFormData ] = useState<TPublicTagItem>(); const [ formData, setFormData ] = useState<PublicTag>();
const toast = useToast(); const toast = useToast();
...@@ -39,7 +40,7 @@ const PublicTags: React.FC = () => { ...@@ -39,7 +40,7 @@ const PublicTags: React.FC = () => {
}); });
}, [ toast ]); }, [ toast ]);
const changeToFormScreen = useCallback((data?: TPublicTagItem) => { const changeToFormScreen = useCallback((data?: PublicTag) => {
setFormData(data); setFormData(data);
setScreen('form'); setScreen('form');
animateScroll.scrollToTop({ animateScroll.scrollToTop({
...@@ -82,4 +83,4 @@ const PublicTags: React.FC = () => { ...@@ -82,4 +83,4 @@ const PublicTags: React.FC = () => {
); );
}; };
export default PublicTags; export default PublicTagsComponent;
import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react'; import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { TPublicTag } from 'data/publicTags'; import type { PublicTags, PublicTag } from 'types/api/account';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
tags: Array<TPublicTag>; data: PublicTag;
onDeleteSuccess: () => void; onDeleteSuccess: () => void;
} }
const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onDeleteSuccess }) => { const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDeleteSuccess }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', tags);
onDeleteSuccess();
}, [ tags, onDeleteSuccess ]);
const [ reason, setReason ] = useState<string>(''); const [ reason, setReason ] = useState<string>('');
const tags = data.tags.split(';');
const queryClient = useQueryClient();
const deleteApiKey = (reason: string) => {
const body = JSON.stringify({ remove_reason: reason });
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
};
const mutation = useMutation(deleteApiKey, {
onSuccess: async() => {
queryClient.setQueryData([ 'public-tags' ], (prevData: PublicTags | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
onClose();
},
// eslint-disable-next-line no-console
onError: console.error,
});
const onDelete = useCallback(() => {
mutation.mutate(reason);
onDeleteSuccess();
}, [ reason, mutation, onDeleteSuccess ]);
const onFieldChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => { const onFieldChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setReason(event.currentTarget.value); setReason(event.currentTarget.value);
}, []); }, []);
...@@ -31,7 +54,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -31,7 +54,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
text = ( text = (
<> <>
<Text display="flex">Public tag</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> <Text>will be removed.</Text>
</> </>
); );
...@@ -40,15 +63,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -40,15 +63,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
const tagsText: Array<JSX.Element | string> = []; const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => { tags.forEach((tag, index) => {
if (index < tags.length - 2) { 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(','); tagsText.push(',');
} }
if (index === tags.length - 2) { 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'); tagsText.push('and');
} }
if (index === tags.length - 1) { 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 = ( text = (
...@@ -81,8 +104,9 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -81,8 +104,9 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
onDelete={ onDelete } onDelete={ onDelete }
title="Request to remove a public tag" title="Request to remove a public tag"
renderContent={ renderContent } renderContent={ renderContent }
pending={ mutation.isLoading }
/> />
); );
}; };
export default DeletePublicTagModal; export default React.memo(DeletePublicTagModal);
...@@ -8,14 +8,14 @@ import { ...@@ -8,14 +8,14 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TPublicTagItem, TPublicTags } from 'data/publicTags'; import type { PublicTags, PublicTag } from 'types/api/account';
import PublicTagTableItem from './PublicTagTableItem'; import PublicTagTableItem from './PublicTagTableItem';
interface Props { interface Props {
data: TPublicTags; data: PublicTags;
onEditClick: (data: TPublicTagItem) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: TPublicTagItem) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
...@@ -24,14 +24,14 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { ...@@ -24,14 +24,14 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th width="60%">Smart contract / Address (0x...)</Th> <Th width="50%">Smart contract / Address (0x...)</Th>
<Th width="40%">Public tag</Th> <Th width="25%">Public tag</Th>
<Th width="200px">Submission date</Th> <Th width="25%">Request status</Th>
<Th width="108px"></Th> <Th width="108px"></Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item: TPublicTagItem) => ( { data.map((item) => (
<PublicTagTableItem <PublicTagTableItem
item={ item } item={ item }
key={ item.id } key={ item.id }
......
import { import {
Box, Box,
Tag, Tag,
Text,
Tr, Tr,
Td, Td,
HStack, HStack,
VStack, VStack,
useColorModeValue, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from '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 AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
...@@ -18,9 +18,9 @@ import EditButton from 'ui/shared/EditButton'; ...@@ -18,9 +18,9 @@ import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: TPublicTagItem; item: PublicTag;
onEditClick: (data: TPublicTagItem) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: TPublicTagItem) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
...@@ -32,19 +32,18 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -32,19 +32,18 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return onDeleteClick(item); return onDeleteClick(item);
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<VStack spacing={ 4 } alignItems="unset"> <VStack spacing={ 4 } alignItems="unset">
{ item.addresses.map((adr: TPublicTagAddress) => { { item.addresses.split(';').map((address) => {
return ( return (
<HStack spacing={ 4 } key={ adr.address } overflow="hidden" alignItems="start"> <HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start">
<AddressIcon address={ adr.address }/> <AddressIcon address={ address }/>
<Box overflow="hidden"> <Box overflow="hidden">
<AddressLinkWithTooltip address={ adr.address }/> <AddressLinkWithTooltip address={ address }/>
{ adr.addressName && <Text fontSize="sm" color={ secondaryColor } mt={ 0.5 }>{ adr.addressName }</Text> } { /* will be added later */ }
{ /* <Text fontSize="sm" variant="secondary" mt={ 0.5 }>Address Name</Text> */ }
</Box> </Box>
</HStack> </HStack>
); );
...@@ -53,11 +52,11 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -53,11 +52,11 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Td> </Td>
<Td> <Td>
<VStack spacing={ 2 } alignItems="baseline"> <VStack spacing={ 2 } alignItems="baseline">
{ item.tags.map((tag: TPublicTag) => { { item.tags.split(';').map((tag) => {
return ( return (
<TruncatedTextTooltip label={ tag.name } key={ tag.name }> <TruncatedTextTooltip label={ tag } key={ tag }>
<Tag color={ tag.colorHex || 'gray.600' } background={ tag.backgroundHex || 'gray.200' } lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ tag.name } { tag }
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
); );
...@@ -65,7 +64,9 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -65,7 +64,9 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<Text fontSize="sm" color={ secondaryColor }>{ item.date }</Text> <VStack alignItems="flex-start">
<Text fontSize="sm" fontWeight="500">Submitted</Text>
</VStack>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
...@@ -77,4 +78,4 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -77,4 +78,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 React, { useCallback, useState } from 'react';
import type { TPublicTagItem, TPublicTag } from 'data/publicTags'; import type { PublicTags, PublicTag } from 'types/api/account';
import { publicTags } from 'data/publicTags';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal'; import DeletePublicTagModal from './DeletePublicTagModal';
import PublicTagTable from './PublicTagTable/PublicTagTable'; import PublicTagTable from './PublicTagTable/PublicTagTable';
type Props = { type Props = {
changeToFormScreen: (data?: TPublicTagItem) => void; changeToFormScreen: (data?: PublicTag) => void;
onTagDelete: () => void; onTagDelete: () => void;
} }
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); 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(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData([]); setDeleteModalData(undefined);
deleteModalProps.onClose(); deleteModalProps.onClose();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
...@@ -25,15 +35,41 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -25,15 +35,41 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
changeToFormScreen(); changeToFormScreen();
}, [ changeToFormScreen ]); }, [ changeToFormScreen ]);
const onItemEditClick = useCallback((item: TPublicTagItem) => { const onItemEditClick = useCallback((item: PublicTag) => {
changeToFormScreen(item); changeToFormScreen(item);
}, [ changeToFormScreen ]); }, [ changeToFormScreen ]);
const onItemDeleteClick = useCallback((item: TPublicTagItem) => { const onItemDeleteClick = useCallback((item: PublicTag) => {
setDeleteModalData(item.tags); setDeleteModalData(item);
deleteModalProps.onOpen(); deleteModalProps.onOpen();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const content = (() => {
if (isLoading || isError) {
return (
<>
<SkeletonTable columns={ [ '50%', '25%', '25%', '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"
size="lg"
onClick={ changeToForm }
>
Request to add public tag
</Button>
</Box>
</>
);
})();
return ( return (
<> <>
<Text marginBottom={ 12 }> <Text marginBottom={ 12 }>
...@@ -42,22 +78,15 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -42,22 +78,15 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
Clicking a tag opens a page with related information and helps provide context and data organization. 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. Requests are sent to a moderator for review and approval. This process can take several days.
</Text> </Text>
<PublicTagTable data={ publicTags } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> { content }
<Box marginTop={ 8 }> { deleteModalData && (
<Button <DeletePublicTagModal
variant="primary" { ...deleteModalProps }
size="lg" onClose={ onDeleteModalClose }
onClick={ changeToForm } data={ deleteModalData }
> onDeleteSuccess={ onTagDelete }
Request to add public tag />
</Button> ) }
</Box>
<DeletePublicTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
tags={ deleteModalData }
onDeleteSuccess={ onTagDelete }
/>
</> </>
); );
}; };
......
...@@ -7,24 +7,24 @@ import type { Inputs } from './PublicTagsForm'; ...@@ -7,24 +7,24 @@ import type { Inputs } from './PublicTagsForm';
interface Props { interface Props {
control: Control<Inputs>; control: Control<Inputs>;
canReport: boolean; isDisabled?: boolean;
} }
export default function PublicTagFormAction({ control, canReport }: Props) { export default function PublicTagFormAction({ control, isDisabled }: Props) {
const renderRadioGroup = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'action'>}) => { const renderRadioGroup = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'action'>}) => {
return ( return (
<RadioGroup defaultValue="add" value={ field.value } colorScheme="blue"> <RadioGroup defaultValue="add" colorScheme="blue" { ...field }>
<Stack spacing={ 5 }> <Stack spacing={ 5 }>
<Radio value="add"> <Radio value="add">
I want to add tags for my project I want to add tags for my project
</Radio> </Radio>
<Radio value="report" isDisabled={ canReport }> <Radio value="report" isDisabled={ isDisabled }>
I want to report an incorrect public tag I want to report an incorrect public tag
</Radio> </Radio>
</Stack> </Stack>
</RadioGroup> </RadioGroup>
); );
}, [ canReport ]); }, [ isDisabled ]);
return ( return (
<Controller <Controller
......
...@@ -6,11 +6,13 @@ import { ...@@ -6,11 +6,13 @@ import {
Text, Text,
HStack, HStack,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { Path } from 'react-hook-form'; import type { Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import type { TPublicTagItem, TPublicTag, TPublicTagAddress } from 'data/publicTags'; import type { PublicTags, PublicTag, PublicTagNew } from 'types/api/account';
import { EMAIL_REGEXP } from 'lib/validations/email'; import { EMAIL_REGEXP } from 'lib/validations/email';
import PublicTagFormAction from './PublicTagFormAction'; import PublicTagFormAction from './PublicTagFormAction';
...@@ -20,45 +22,48 @@ import PublicTagsFormInput from './PublicTagsFormInput'; ...@@ -20,45 +22,48 @@ import PublicTagsFormInput from './PublicTagsFormInput';
type Props = { type Props = {
changeToDataScreen: (success?: boolean) => void; changeToDataScreen: (success?: boolean) => void;
data?: TPublicTagItem; data?: PublicTag;
} }
export type Inputs = { export type Inputs = {
userName: string; fullName?: string;
userEmail: string; email?: string;
companyName: string; companyName?: string;
companyUrl: string; companyUrl?: string;
action: 'add' | 'report'; action: 'add' | 'report';
tag: string; tags?: string;
addresses: Array<{ addresses?: Array<{
name: string; name: string;
address: string; address: string;
}>; }>;
comment: string; comment?: string;
} }
const placeholders = { const placeholders = {
userName: 'Your name', fullName: 'Your name',
userEmail: 'Email', email: 'Email',
companyName: 'Company name', companyName: 'Company name',
companyUrl: 'Company website', companyUrl: 'Company website',
tag: 'Public tag (max 35 characters)', tags: 'Public tag (max 35 characters)',
comment: 'Specify the reason for adding tags and color preference(s).', comment: 'Specify the reason for adding tags and color preference(s).',
} as Record<Path<Inputs>, string>; } as Record<Path<Inputs>, string>;
const ADDRESS_INPUT_BUTTONS_WIDTH = 170; const ADDRESS_INPUT_BUTTONS_WIDTH = 170;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({
defaultValues: { defaultValues: {
userName: data?.userName || '', fullName: data?.full_name || '',
userEmail: data?.userEmail || '', email: data?.email || '',
companyName: data?.companyName || '', companyName: data?.company || '',
companyUrl: data?.companyUrl || '', companyUrl: data?.website || '',
tag: data?.tags.map((tag: TPublicTag) => tag.name).join('; ') || '', tags: data?.tags.split(';').map((tag) => tag).join('; ') || '',
addresses: data?.addresses.map((adr: TPublicTagAddress, index: number) => ({ name: `address.${ index }.address`, address: adr.address })) || addresses: data?.addresses.split(';').map((address, index: number) => ({ name: `address.${ index }.address`, address })) ||
[ { name: 'address.0.address', address: '' } ], [ { name: 'address.0.address', address: '' } ],
comment: data?.comment || '', comment: data?.additional_comment || '',
action: data?.is_owner === undefined || data?.is_owner ? 'add' : 'report',
}, },
mode: 'all', mode: 'all',
}); });
...@@ -69,10 +74,61 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -69,10 +74,61 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}); });
const onAddFieldClick = useCallback(() => append({ address: '' }), [ append ]); const onAddFieldClick = useCallback(() => append({ address: '' }), [ append ]);
const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]); const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]);
const updatePublicTag = (formData: Inputs) => {
const payload: PublicTagNew = {
full_name: formData.fullName || '',
email: formData.email || '',
company: formData.companyName || '',
website: formData.companyUrl || '',
is_owner: formData.action === 'add',
addresses_array: formData.addresses?.map(({ address }) => address) || [],
tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '',
additional_comment: formData.comment || '',
};
const body = JSON.stringify(payload);
if (!data?.id) {
return fetch('/api/account/public-tags', { method: 'POST', body });
}
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'PUT', body });
};
const mutation = useMutation(updatePublicTag, {
onSuccess: async(data) => {
const response: PublicTag = await data.json();
queryClient.setQueryData([ 'public-tags' ], (prevData: PublicTags | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) {
return prevData.map((item) => {
if (item.id === response.id) {
return response;
}
return item;
});
}
return [ ...(prevData || []), response ];
});
changeToDataScreen(true);
},
// eslint-disable-next-line no-console
onError: console.error,
});
const onSubmit: SubmitHandler<Inputs> = useCallback((data) => {
mutation.mutate(data);
}, [ mutation ]);
const changeToData = useCallback(() => { const changeToData = useCallback(() => {
changeToDataScreen(true); changeToDataScreen(false);
}, [ changeToDataScreen ]); }, [ changeToDataScreen ]);
return ( return (
...@@ -81,9 +137,9 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -81,9 +137,9 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Grid templateColumns="1fr 1fr" rowGap={ 4 } columnGap={ 5 }> <Grid templateColumns="1fr 1fr" rowGap={ 4 } columnGap={ 5 }>
<GridItem> <GridItem>
<PublicTagsFormInput<Inputs> <PublicTagsFormInput<Inputs>
fieldName="userName" fieldName="fullName"
control={ control } control={ control }
label={ placeholders.userName } label={ placeholders.fullName }
required required
/> />
</GridItem> </GridItem>
...@@ -96,11 +152,11 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -96,11 +152,11 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
</GridItem> </GridItem>
<GridItem> <GridItem>
<PublicTagsFormInput<Inputs> <PublicTagsFormInput<Inputs>
fieldName="userEmail" fieldName="email"
control={ control } control={ control }
label={ placeholders.userEmail } label={ placeholders.email }
pattern={ EMAIL_REGEXP } pattern={ EMAIL_REGEXP }
hasError={ Boolean(errors.userEmail) } hasError={ Boolean(errors.email) }
required required
/> />
</GridItem> </GridItem>
...@@ -113,15 +169,15 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -113,15 +169,15 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
</GridItem> </GridItem>
</Grid> </Grid>
<Box marginTop={ 4 } marginBottom={ 8 }> <Box marginTop={ 4 } marginBottom={ 8 }>
<PublicTagFormAction canReport={ Boolean(data) } control={ control }/> <PublicTagFormAction control={ control }/>
</Box> </Box>
<Text size="sm" variant="secondary" marginBottom={ 5 }>Public tags (2 tags maximum, please use &quot;;&quot; as a divider)</Text> <Text size="sm" variant="secondary" marginBottom={ 5 }>Public tags (2 tags maximum, please use &quot;;&quot; as a divider)</Text>
<Box marginBottom={ 4 }> <Box marginBottom={ 4 }>
<PublicTagsFormInput<Inputs> <PublicTagsFormInput<Inputs>
fieldName="tag" fieldName="tags"
control={ control } control={ control }
label={ placeholders.tag } label={ placeholders.tags }
hasError={ Boolean(errors.tag) } hasError={ Boolean(errors.tags) }
required/> required/>
</Box> </Box>
{ fields.map((field, index) => { { fields.map((field, index) => {
...@@ -145,8 +201,8 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -145,8 +201,8 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button <Button
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(changeToData) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ Object.keys(errors).length > 0 || mutation.isLoading }
> >
Send request Send request
</Button> </Button>
...@@ -162,4 +218,4 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -162,4 +218,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