-
Notifications
You must be signed in to change notification settings - Fork 12
Feature/12088 new file upload #13229
base: develop
Are you sure you want to change the base?
Changes from all commits
6466949
3185823
5555f2c
f146f15
7f02322
e08c2a5
1928c3c
2a1781c
dc6e941
394ea0a
f932c14
89d0261
34e7eb8
b17514c
a458550
710db37
b3fff72
52c9918
d88580a
004c1c9
0617527
ec54034
32ba221
eadd5fe
2a8a0f0
0517200
3cae0e0
773063f
ee1388f
e94b3cb
88ab690
688822a
aa6577b
93f17f0
3d5cb87
7226177
cc6ff8a
99d338f
8dd937d
6e890de
4795f7c
822949f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import React, { FC } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Image, Pressable } from 'react-native' | ||
| import { Asset } from 'react-native-image-picker' | ||
|
|
||
| import { Icon } from '@department-of-veterans-affairs/mobile-component-library' | ||
| import styled from 'styled-components' | ||
|
|
||
| import { Box, TextView } from 'components/index' | ||
| import { NAMESPACE } from 'constants/namespaces' | ||
| import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens' | ||
| import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common' | ||
| import { useShowActionSheet, useTheme } from 'utils/hooks' | ||
| import { themeFn } from 'utils/theme' | ||
|
|
||
| export type FilePreviewProps = { | ||
| /** Response object from file selection with file name and size information */ | ||
| file: Asset | DocumentPickerResponse | ||
| /** Callback when the delete button is pressed */ | ||
| onDelete: () => void | ||
| /** Accessibility label for the preview element */ | ||
| a11yLabel: string | ||
| } | ||
|
|
||
| type StyledImageProps = { | ||
| /** prop to set image width */ | ||
| width: number | ||
| /** prop to set image height */ | ||
| height: number | ||
| /** Hardcoded radius of 5 due to design plan */ | ||
| borderRadius: number | ||
| } | ||
|
|
||
| const StyledImage = styled(Image)<StyledImageProps>` | ||
| width: ${themeFn<StyledImageProps>((_theme, props) => props.width)}px; | ||
| height: ${themeFn<StyledImageProps>((_theme, props) => props.height)}px; | ||
| border-radius: ${themeFn<StyledImageProps>((_theme, props) => props.borderRadius)}px; | ||
| ` | ||
|
|
||
| const FilePreview: FC<FilePreviewProps> = ({ file, onDelete, a11yLabel }) => { | ||
| const { t } = useTranslation(NAMESPACE.COMMON) | ||
| const theme = useTheme() | ||
| const confirmAlert = useShowActionSheet() | ||
|
|
||
| const isImage = !('fileCopyUri' in file) | ||
| const fileName = isImage ? file.fileName : (file as DocumentPickerResponse).name | ||
| const fileSize = isImage ? file.fileSize : (file as DocumentPickerResponse).size | ||
| const imageUri = isImage ? file.uri : undefined | ||
| const fileSizeDisplayText = fileSize ? bytesToFinalSizeDisplay(fileSize, t, false) : '' | ||
| const fileSizeA11yText = fileSize ? bytesToFinalSizeDisplayA11y(fileSize, t, false) : '' | ||
| const accessibilityLabel = [a11yLabel, fileName, fileSizeA11yText].filter(Boolean).join(', ') | ||
|
|
||
| const onPress = (): void => { | ||
| const options = [t('remove'), t('keep')] | ||
|
|
||
| confirmAlert( | ||
| { | ||
| options, | ||
| title: t('file.removeFile'), | ||
| destructiveButtonIndex: 0, | ||
| }, | ||
| (buttonIndex) => { | ||
| switch (buttonIndex) { | ||
| case 0: | ||
| onDelete() | ||
| break | ||
| case 1: | ||
| break | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <Pressable | ||
| onPress={onPress} | ||
| accessibilityRole="button" | ||
| accessibilityLabel={accessibilityLabel} | ||
| accessibilityHint={t('fileUpload.delete.a11yHint')}> | ||
| <Box flexDirection="row" gap={10}> | ||
| {imageUri ? ( | ||
| <StyledImage | ||
| source={{ uri: imageUri }} | ||
| width={theme.dimensions.imagePreviewWidth} | ||
| height={theme.dimensions.imagePreviewWidth} | ||
| borderRadius={5} | ||
| /> | ||
| ) : ( | ||
| <Box width={theme.dimensions.imagePreviewWidth}> | ||
| <Icon | ||
| name={'DescriptionOutlined'} | ||
| fill={theme.colors.text.primary} | ||
| width={theme.dimensions.filePreviewWidth} | ||
| height={theme.dimensions.filePreviewWidth} | ||
| preventScaling={true} | ||
| /> | ||
| </Box> | ||
| )} | ||
| <Box flex={1}> | ||
| <TextView variant="HelperTextBold">{fileName}</TextView> | ||
| <TextView variant="HelperText">{fileSizeDisplayText}</TextView> | ||
| </Box> | ||
| <Box> | ||
| <Icon name={'Delete'} fill={theme.colors.icon.error} width={24} height={25} preventScaling={true} /> | ||
| </Box> | ||
| </Box> | ||
| </Pressable> | ||
| ) | ||
| } | ||
|
|
||
| export default FilePreview |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import React, { FC, useMemo } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Asset } from 'react-native-image-picker' | ||
|
|
||
| import { Button, ButtonVariants, Icon } from '@department-of-veterans-affairs/mobile-component-library' | ||
|
|
||
| import FilePreview from 'components/FileUpload/FilePreview' | ||
| import { Box, BoxProps, TextView, VABulletListText } from 'components/index' | ||
| import { FileValidationConfig } from 'constants/fileUpload' | ||
| import { NAMESPACE } from 'constants/namespaces' | ||
| import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens' | ||
| import theme from 'styles/themes/standardTheme' | ||
| import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common' | ||
| import { getFileUris, getImageBase64s, getTotalBytesUsedByFiles, onAddMultipleFileAttachments } from 'utils/fileUpload' | ||
| import { useShowActionSheet } from 'utils/hooks' | ||
|
|
||
| export type FileUploadProps = { | ||
| /** i18n key for the text label next to the file upload field */ | ||
| labelKey?: string | ||
| /** Optional list of Assets or DocumentPickerResponse objects to display */ | ||
| fileList?: Array<Asset | DocumentPickerResponse> | ||
| /** Callback after a successful file selection is made */ | ||
| onFileSelection: (response: Array<Asset | DocumentPickerResponse>) => void | ||
| /** Optional callback when a file's delete button is pressed */ | ||
| onDeleteFile?: (idx: number) => void | ||
| /** Callback to return an error message after an invalid file selection */ | ||
| setErrors: (value: Array<VABulletListText>) => void | ||
| /** Config obj that defines the file selection parameters */ | ||
| validationConfig: FileValidationConfig | ||
| } | ||
|
|
||
| const FileUpload: FC<FileUploadProps> = ({ | ||
| labelKey, | ||
| fileList = [], | ||
| onFileSelection, | ||
| onDeleteFile, | ||
| setErrors, | ||
| validationConfig, | ||
| }) => { | ||
| const showActionSheetWithOptions = useShowActionSheet() | ||
| const { t } = useTranslation(NAMESPACE.COMMON) | ||
|
|
||
| const totalBytesUsed = useMemo(() => getTotalBytesUsedByFiles(fileList), [fileList]) | ||
|
|
||
| const containerProps: BoxProps = { | ||
| borderColor: 'photoAdd', | ||
| borderWidth: 2, | ||
| borderStyle: 'dashed', | ||
| borderRadius: 5, | ||
| gap: 16, | ||
| px: 20, | ||
| py: 16, | ||
| } | ||
|
|
||
| const onChooseAFile = (): void => { | ||
| onAddMultipleFileAttachments( | ||
| t, | ||
| showActionSheetWithOptions, | ||
| setErrors, | ||
| onFileSelection, | ||
| getTotalBytesUsedByFiles(fileList), | ||
| getFileUris(fileList), | ||
| getImageBase64s(fileList), | ||
| validationConfig, | ||
| ) | ||
| } | ||
|
|
||
| const getFileList = () => { | ||
| if (fileList.length < 1) { | ||
| return ( | ||
| <Box alignItems="center"> | ||
| <Icon name={'UploadFile'} width={56} height={56} preventScaling={true} /> | ||
| </Box> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {fileList.map((file, idx) => { | ||
| return ( | ||
| <FilePreview | ||
| key={idx} | ||
| file={file} | ||
| onDelete={() => onDeleteFile?.(idx)} | ||
| a11yLabel={t('fileUpload.ofTotalFiles', { | ||
|
Comment on lines
+79
to
+85
|
||
| fileNum: idx + 1, | ||
| count: fileList.length, | ||
| })} | ||
| /> | ||
| ) | ||
| })} | ||
| <Box | ||
| flexDirection="row" | ||
| borderTopColor="divider" | ||
| justifyContent="space-between" | ||
| borderTopWidth={2} | ||
| pt={8} | ||
| px={20}> | ||
| <TextView variant="HelperText" color={'fileInfo'}> | ||
| {t('fileUpload.ofTenFiles', { numOfFiles: fileList?.length })} | ||
| </TextView> | ||
| {/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/} | ||
| <TextView | ||
| variant="HelperText" | ||
| color={'fileInfo'} | ||
| accessibilityLabel={t('fileUpload.ofFiftyMB.a11y', { | ||
| sizeOfPhotos: bytesToFinalSizeDisplayA11y(totalBytesUsed ? totalBytesUsed : 0, t, false), | ||
| })}> | ||
| {t('fileUpload.ofFiftyMB', { | ||
| sizeOfPhotos: bytesToFinalSizeDisplay(totalBytesUsed ? totalBytesUsed : 0, t, false), | ||
| })} | ||
| </TextView> | ||
|
Comment on lines
+99
to
+112
|
||
| </Box> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {labelKey && ( | ||
| <Box display="flex" flexDirection="row" flexWrap="wrap" mb={theme.dimensions.smallMarginBetween}> | ||
| <TextView>{t(labelKey)}</TextView> | ||
| </Box> | ||
| )} | ||
| <Box {...containerProps}> | ||
| <Box> | ||
| {/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/} | ||
| <TextView | ||
| variant="HelperText" | ||
| color={'placeholder'} | ||
| accessibilityLabel={t('fileUpload.acceptedFileSpecs.a11y')}> | ||
| {t('fileUpload.acceptedFileSpecs')} | ||
| </TextView> | ||
| </Box> | ||
| {getFileList()} | ||
| {fileList.length < validationConfig.maxFileQuantity && ( | ||
| <Button | ||
| label={fileList.length < 1 ? t('fileUpload.chooseAFile') : t('fileUpload.attachFiles')} | ||
| onPress={onChooseAFile} | ||
| buttonType={ButtonVariants.Secondary} | ||
| /> | ||
| )} | ||
| </Box> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| export default FileUpload | ||
|
AdryienH marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { isValidFileType } from 'utils/claims' | ||
| import { isValidAttachmentsFileType } from 'utils/secureMessaging' | ||
|
|
||
| export type FileValidationConfig = { | ||
| maxFileQuantity: number | ||
| maxSingleFileBytes?: number | ||
| maxTotalBytes: number | ||
| isAllowedMimeType: (mime: string) => boolean | ||
| } | ||
|
|
||
| export const MAX_IMAGE_DIMENSION = 1375 | ||
|
|
||
| export const MAX_SINGLE_MESSAGE_ATTACHMENT_SIZE_IN_BYTES = 6291456 | ||
| export const MAX_TOTAL_MESSAGE_ATTACHMENTS_SIZE_IN_BYTES = 10485760 | ||
| export const MAX_FILE_QUANTITY_SECURE_MESSAGING = 4 | ||
|
|
||
| export const MAX_TOTAL_FILE_SIZE_IN_BYTES_CLAIMS = 52428800 | ||
| export const MAX_FILE_QUANTITY_CLAIMS = 10 | ||
|
|
||
| export const CLAIMS_FILE_VALIDATION_CONFIG: FileValidationConfig = { | ||
| maxFileQuantity: MAX_FILE_QUANTITY_CLAIMS, | ||
| maxTotalBytes: MAX_TOTAL_FILE_SIZE_IN_BYTES_CLAIMS, | ||
| isAllowedMimeType: isValidFileType, | ||
| } | ||
|
|
||
| export const SECURE_MESSAGING_FILE_VALIDATION_CONFIG: FileValidationConfig = { | ||
| maxFileQuantity: MAX_FILE_QUANTITY_SECURE_MESSAGING, | ||
| maxSingleFileBytes: MAX_SINGLE_MESSAGE_ATTACHMENT_SIZE_IN_BYTES, | ||
| maxTotalBytes: MAX_TOTAL_MESSAGE_ATTACHMENTS_SIZE_IN_BYTES, | ||
| isAllowedMimeType: isValidAttachmentsFileType, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.