Commit fc54f262 authored by tom's avatar tom

sources field changes

parent e128c78f
<svg viewBox="0 0 51 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.693 5.058c.498-.57 1.172-.891 1.875-.891h15.91c.351 0 .688.16.937.446l9.28 10.657c.249.285.388.672.388 1.076v24.36c0 .807-.279 1.581-.776 2.152-.498.571-1.172.892-1.875.892H13.568c-.703 0-1.377-.32-1.875-.892-.497-.57-.776-1.345-.776-2.153V7.212c0-.808.28-1.582.776-2.154Zm17.235 2.154h-15.36v33.493h23.864V16.977l-8.504-9.765Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.556 5c.767 0 1.389.622 1.389 1.389v8.333h8.333a1.389 1.389 0 0 1 0 2.778h-9.722a1.389 1.389 0 0 1-1.39-1.389V6.39c0-.767.623-1.389 1.39-1.389ZM22.46 25.151a1.326 1.326 0 0 0-1.875 1.875l3.04 3.04-3.04 3.04a1.326 1.326 0 0 0 1.875 1.875l3.04-3.04 3.04 3.04a1.326 1.326 0 0 0 1.875-1.875l-3.04-3.04 3.04-3.04a1.326 1.326 0 0 0-1.875-1.875l-3.04 3.04-3.04-3.04Z" fill="currentColor"/>
</svg>
import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react'; import { Text, Button, Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts'; import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea'; import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError'; import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/FileSnippet';
...@@ -19,11 +19,10 @@ interface Props { ...@@ -19,11 +19,10 @@ interface Props {
fileTypes: Array<FileTypes>; fileTypes: Array<FileTypes>;
multiple?: boolean; multiple?: boolean;
title: string; title: string;
className?: string;
hint: string; hint: string;
} }
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => { const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>(); const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined; const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
}, [ getValues, clearErrors, setValue ]); }, [ getValues, clearErrors, setValue ]);
const renderUploadButton = React.useCallback(() => {
return (
<div>
<Text fontWeight={ 500 } color="text_secondary" mb={ 3 }>{ title }</Text>
<Button size="sm" variant="outline">
Drop file{ multiple ? 's' : '' } or click here
</Button>
</div>
);
}, [ multiple, title ]);
const renderFiles = React.useCallback((files: Array<File>) => { const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';'); const errorList = fileError?.message?.split(';');
return ( return (
<Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }> <Box
display="grid"
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }}
columnGap={ 3 }
rowGap={ 3 }
w="100%"
>
{ files.map((file, index) => ( { files.map((file, index) => (
<Box key={ file.name + file.lastModified + index }> <Box key={ file.name + file.lastModified + index }>
<FileSnippet <FileSnippet
...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
onRemove={ handleFileRemove } onRemove={ handleFileRemove }
index={ index } index={ index }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
error={ errorList?.[index] }
/> />
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box> </Box>
)) } )) }
</Box> </Box>
); );
}, [ formState.isSubmitting, handleFileRemove, fileError ]); }, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => ( const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => {
const hasValue = field.value && field.value.length > 0;
return (
<> <>
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }> <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ () => ( { ({ onChange }) => (
<Flex <Flex
flexDir="column" flexDir="column"
alignItems="flex-start" alignItems="flex-start"
rowGap={ 2 } rowGap={ 2 }
w="100%" w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
> >
<Button <DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }}>
variant="outline" { hasValue ? renderFiles(field.value) : renderUploadButton() }
size="sm" </DragAndDropArea>
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex> </Flex>
) } ) }
</FileInput> </FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> } { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
</> </>
), [ fileTypes, commonError, multiple, renderFiles ]); );
}, [ fileTypes, multiple, renderFiles, commonError, renderUploadButton ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => { const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
return true; return true;
}, []); }, []);
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (!multiple && Array.isArray(value) && value.length > 1) {
return 'You can upload only one file';
}
return true;
}, [ multiple ]);
const rules = React.useMemo(() => ({ const rules = React.useMemo(() => ({
required: true, required: true,
validate: { validate: {
file_type: validateFileType, file_type: validateFileType,
file_size: validateFileSize, file_size: validateFileSize,
quantity: validateQuantity,
}, },
}), [ validateFileSize, validateFileType ]); }), [ validateFileSize, validateFileType, validateQuantity ]);
return ( return (
<>
<ContractVerificationFormRow >
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text>
</ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="sources" name="sources"
...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
/> />
{ hint ? <span>{ hint }</span> : null } { hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
</>
); );
}; };
export default React.memo(chakra(ContractVerificationFieldSources)); export default React.memo(ContractVerificationFieldSources);
...@@ -12,7 +12,7 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio ...@@ -12,7 +12,7 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
const ContractVerificationFlattenSourceCode = () => { const ContractVerificationFlattenSourceCode = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/> <ContractVerificationFieldIsYul/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
......
...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ]; ...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => { const ContractVerificationMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (multi-part files)">
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
......
...@@ -7,11 +7,11 @@ const FILE_TYPES = [ '.json' as const, '.sol' as const ]; ...@@ -7,11 +7,11 @@ const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => { const ContractVerificationSourcify = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
<ContractVerificationFieldSources <ContractVerificationFieldSources
fileTypes={ FILE_TYPES } fileTypes={ FILE_TYPES }
multiple multiple
title="Sources and Metadata JSON" mt={ 0 } title="Sources and Metadata JSON"
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation." hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
/> />
</ContractVerificationMethod> </ContractVerificationMethod>
......
...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ]; ...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => { const ContractVerificationStandardInput = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa ...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = () => {
return ( return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code."/> <ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
......
...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ]; ...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => { const ContractVerificationVyperMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/> <ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
import { Box } from '@chakra-ui/react'; import { chakra, Center } from '@chakra-ui/react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import React from 'react'; import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files'; import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props { interface Props {
children: React.ReactNode;
onDrop: (files: Array<File>) => void; onDrop: (files: Array<File>) => void;
className?: string;
} }
const DragAndDropArea = ({ onDrop }: Props) => { const DragAndDropArea = ({ onDrop, children, className }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false); const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => { const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
...@@ -36,17 +38,27 @@ const DragAndDropArea = ({ onDrop }: Props) => { ...@@ -36,17 +38,27 @@ const DragAndDropArea = ({ onDrop }: Props) => {
}, []); }, []);
return ( return (
<Box <Center
className={ className }
w="100%" w="100%"
h="200px" minH="120px"
bgColor="lightpink" borderWidth="2px"
opacity={ isDragOver ? 0.8 : 1 } borderColor={ isDragOver ? 'link_hovered' : 'link' }
_hover={{
borderColor: 'link_hovered',
}}
borderRadius="base"
borderStyle="dashed"
cursor="pointer"
textAlign="center"
onDrop={ handleDrop } onDrop={ handleDrop }
onDragOver={ handleDragOver } onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter } onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave } onDragLeave={ handleDragLeave }
/> >
{ children }
</Center>
); );
}; };
export default React.memo(DragAndDropArea); export default React.memo(chakra(DragAndDropArea));
import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra } from '@chakra-ui/react'; import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CrossIcon from 'icons/cross.svg'; import CrossIcon from 'icons/cross.svg';
import imageIcon from 'icons/files/image.svg';
import jsonIcon from 'icons/files/json.svg'; import jsonIcon from 'icons/files/json.svg';
import placeholderIcon from 'icons/files/placeholder.svg';
import solIcon from 'icons/files/sol.svg'; import solIcon from 'icons/files/sol.svg';
import yulIcon from 'icons/files/yul.svg'; import yulIcon from 'icons/files/yul.svg';
import infoIcon from 'icons/info.svg';
import { shortenNumberWithLetter } from 'lib/formatters'; import { shortenNumberWithLetter } from 'lib/formatters';
const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = { const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
...@@ -29,31 +30,64 @@ interface Props { ...@@ -29,31 +30,64 @@ interface Props {
index?: number; index?: number;
onRemove?: (index?: number) => void; onRemove?: (index?: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
error?: string;
} }
const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) => { const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Props) => {
const handleRemove = React.useCallback(() => { const handleRemove = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onRemove?.(index); onRemove?.(index);
}, [ index, onRemove ]); }, [ index, onRemove ]);
const handleErrorHintIconClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
const fileExtension = getFileExtension(file.name); const fileExtension = getFileExtension(file.name);
const fileIcon = FILE_ICONS[fileExtension] || imageIcon; const fileIcon = FILE_ICONS[fileExtension] || placeholderIcon;
const iconColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
p={ 3 } maxW="300px"
overflow="hidden"
className={ className }
alignItems="center"
textAlign="left"
>
<Icon
as={ fileIcon }
boxSize="74px"
color={ error ? 'error' : iconColor }
mr={ 2 }
borderWidth="2px" borderWidth="2px"
borderRadius="md" borderRadius="md"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
maxW="300px" p={ 3 }
/>
<Box maxW="calc(100% - 58px - 24px)">
<Flex alignItems="center">
<Text
fontWeight={ 600 }
overflow="hidden" overflow="hidden"
className={ className } textOverflow="ellipsis"
whiteSpace="nowrap"
color={ error ? 'error' : 'initial' }
> >
<Icon as={ fileIcon } boxSize="50px" color={ useColorModeValue('gray.600', 'gray.400') } mr={ 2 }/> { file.name }
<Box width="calc(100% - 58px - 24px)" > </Text>
<Text fontWeight={ 600 } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ file.name }</Text> { Boolean(error) && (
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text> <Tooltip
label={ error }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit" onClick={ handleErrorHintIconClick } ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 5 } color="error"/>
</Box> </Box>
</Tooltip>
) }
<IconButton <IconButton
aria-label="remove" aria-label="remove"
icon={ <CrossIcon/> } icon={ <CrossIcon/> }
...@@ -64,8 +98,12 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) => ...@@ -64,8 +98,12 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) =>
ml="auto" ml="auto"
onClick={ handleRemove } onClick={ handleRemove }
isDisabled={ isDisabled } isDisabled={ isDisabled }
alignSelf="flex-start"
/> />
</Flex> </Flex>
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text>
</Box>
</Flex>
); );
}; };
......
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