diff --git a/src/common/components/fileDownloadLink/FileDownloadLink.tsx b/src/common/components/fileDownloadLink/FileDownloadLink.tsx index 38e7aff11..0111c26ea 100644 --- a/src/common/components/fileDownloadLink/FileDownloadLink.tsx +++ b/src/common/components/fileDownloadLink/FileDownloadLink.tsx @@ -1,6 +1,6 @@ +import React, { useEffect, useRef, useState } from 'react'; import { Box } from '@chakra-ui/react'; import { Link, LoadingSpinner, Notification } from 'hds-react'; -import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { QueryFunction, QueryKey, useQueryClient } from 'react-query'; @@ -10,9 +10,17 @@ type Props = { queryKey: QueryKey; queryFunction: QueryFunction; linkIcon?: React.ReactNode; + linkTextStyles?: string; }; -function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIcon }: Props) { +function FileDownloadLink({ + linkText, + fileName, + queryKey, + queryFunction, + linkIcon, + linkTextStyles, +}: Props) { const { t } = useTranslation(); const queryClient = useQueryClient(); const [fileUrl, setFileUrl] = useState(''); @@ -20,6 +28,12 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco const [errorText, setErrorText] = useState(null); const [loading, setLoading] = useState(false); + useEffect(() => { + if (fileUrl) { + linkRef.current?.click(); + } + }, [fileUrl]); + async function fetchFile(event: React.MouseEvent) { setErrorText(null); const cachedUrl = queryClient.getQueryData(queryKey); @@ -35,7 +49,6 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco }); setFileUrl(url); setLoading(false); - linkRef.current?.click(); } catch (error) { setLoading(false); setErrorText(t('common:error')); @@ -49,7 +62,7 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco if (loading) { return ( - + ); @@ -59,7 +72,7 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco <> {linkIcon} - {linkText} + {linkText} {errorText && ( diff --git a/src/common/components/fileUpload/FileList.tsx b/src/common/components/fileUpload/FileList.tsx new file mode 100644 index 000000000..55b2b43a0 --- /dev/null +++ b/src/common/components/fileUpload/FileList.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; +import { IconTrash, Notification } from 'hds-react'; +import { Box } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { AttachmentMetadata } from '../../types/attachment'; +import ConfirmationDialog from '../HDSConfirmationDialog/ConfirmationDialog'; +import FileListItem from './FileListItem'; +import { FileDeleteFunction, FileDownLoadFunction } from './types'; + +type Props = { + files: AttachmentMetadata[]; + fileDownLoadFunction?: FileDownLoadFunction; + fileDeleteFunction: FileDeleteFunction; + onFileDelete?: () => void; +}; + +export default function FileList({ + files, + fileDownLoadFunction, + fileDeleteFunction, + onFileDelete, +}: Props) { + const { t } = useTranslation(); + const deleteMutation = useMutation(fileDeleteFunction, { + onSuccess() { + onFileDelete && onFileDelete(); + }, + }); + const [fileToDelete, setFileToDelete] = useState(null); + const [showFileDeleteDialog, setShowFileDeleteDialog] = useState(false); + + function handleFileDelete(file: AttachmentMetadata) { + setFileToDelete(file); + setShowFileDeleteDialog(true); + } + + function confirmFileDelete() { + deleteMutation.mutate(fileToDelete); + setShowFileDeleteDialog(false); + } + + function closeFileDeleteDialog() { + setFileToDelete(null); + setShowFileDeleteDialog(false); + } + + function closeDeleteSuccessNotification() { + setFileToDelete(null); + deleteMutation.reset(); + } + + return ( + <> + + {files.map((file) => { + return ( + + ); + })} + + + {/* File remove dialog */} + } + variant="danger" + /> + + {/* Attachment delete success notification */} + {deleteMutation.isSuccess && ( + + {t('form:notifications:descriptions:attachmentRemoved', { + fileName: fileToDelete?.fileName, + })} + + )} + + ); +} diff --git a/src/common/components/fileUpload/FileListItem.module.scss b/src/common/components/fileUpload/FileListItem.module.scss new file mode 100644 index 000000000..e668800f9 --- /dev/null +++ b/src/common/components/fileUpload/FileListItem.module.scss @@ -0,0 +1,33 @@ +@import 'src/assets/styles/layout.scss'; + +.fileListItem { + // display: flex; + // align-items: flex-start; + background-color: var(--color-info-light); + border-bottom: 2px dotted var(--color-coat-of-arms); + box-sizing: border-box; + max-width: 100%; + width: 600px; + padding: var(--spacing-s) var(--spacing-2-xs); + flex-wrap: wrap; + + // @include respond-above(l) { + // flex-wrap: nowrap; + // } +} + +.fileListItemName { + // word-break: break-word; + hyphens: auto; +} + +.fileListItemDownloadIcon { + flex-shrink: 0; +} + +.fileListItemButton { + --file-list-item-button-y-offset: calc(-1 * var(--spacing-2-xs) - 2px); + + display: flex; + margin: var(--file-list-item-button-y-offset) auto var(--file-list-item-button-y-offset) 0; +} diff --git a/src/common/components/fileUpload/FileListItem.tsx b/src/common/components/fileUpload/FileListItem.tsx new file mode 100644 index 000000000..7324b5aea --- /dev/null +++ b/src/common/components/fileUpload/FileListItem.tsx @@ -0,0 +1,80 @@ +import { Box, Flex } from '@chakra-ui/react'; +import { format, isToday } from 'date-fns'; +import { fi } from 'date-fns/locale'; +import { Button, IconCross, IconDocument, IconDownload } from 'hds-react'; +import { useTranslation } from 'react-i18next'; +import { AttachmentMetadata } from '../../types/attachment'; +import FileDownloadLink from '../fileDownloadLink/FileDownloadLink'; +import Text from '../text/Text'; +import styles from './FileListItem.module.scss'; +import { FileDownLoadFunction } from './types'; + +type Props = { + fileMetadata: AttachmentMetadata; + onDeleteFile: (file: AttachmentMetadata) => void; + fileDownLoadFunction?: FileDownLoadFunction; + deletingFile?: boolean; +}; + +export default function FileListItem({ + fileMetadata, + onDeleteFile, + fileDownLoadFunction, + deletingFile = false, +}: Props) { + const { t } = useTranslation(); + const showDeleteButton = true; + const dateAdded = new Date(fileMetadata.createdAt); + const dateAddedText = isToday(dateAdded) + ? t('form:labels:today') + : format(dateAdded, 'd.M.yyyy', { locale: fi }); + + return ( + + + + {fileDownLoadFunction === undefined ? ( + + {fileMetadata.fileName} + + ) : ( + <> + fileDownLoadFunction(fileMetadata)} + /> + + + )} + + + {t('form:labels:added')} {dateAddedText} + + {showDeleteButton && ( + + )} + + ); +} diff --git a/src/common/components/fileUpload/FileUpload.test.tsx b/src/common/components/fileUpload/FileUpload.test.tsx index 401c71364..b36b46a3c 100644 --- a/src/common/components/fileUpload/FileUpload.test.tsx +++ b/src/common/components/fileUpload/FileUpload.test.tsx @@ -34,6 +34,7 @@ test('Should upload files successfully and loading indicator is displayed', asyn accept=".png,.jpg" multiple uploadFunction={uploadFunction} + fileDeleteFunction={() => Promise.resolve()} />, ); const fileUpload = screen.getByLabelText(inputLabel); @@ -60,6 +61,7 @@ test('Should show amount of successful files uploaded correctly when file fails accept=".png" multiple uploadFunction={uploadFunction} + fileDeleteFunction={() => Promise.resolve()} />, undefined, undefined, @@ -91,6 +93,7 @@ test('Should show amount of successful files uploaded correctly when request fai accept=".png,.jpg" multiple uploadFunction={uploadFunction} + fileDeleteFunction={() => Promise.resolve()} />, ); const fileUpload = screen.getByLabelText(inputLabel); @@ -113,6 +116,7 @@ test('Should upload files when user drops them into drag-and-drop area', async ( multiple dragAndDrop uploadFunction={uploadFunction} + fileDeleteFunction={() => Promise.resolve()} />, ); fireEvent.drop(screen.getByText('Raahaa tiedostot tänne'), { diff --git a/src/common/components/fileUpload/FileUpload.tsx b/src/common/components/fileUpload/FileUpload.tsx index 9abf96652..287691129 100644 --- a/src/common/components/fileUpload/FileUpload.tsx +++ b/src/common/components/fileUpload/FileUpload.tsx @@ -10,6 +10,8 @@ import { Flex } from '@chakra-ui/react'; import Text from '../text/Text'; import styles from './FileUpload.module.scss'; import { removeDuplicateAttachments } from './utils'; +import FileList from './FileList'; +import { FileDeleteFunction, FileDownLoadFunction } from './types'; function useDragAndDropFiles() { const ref = useRef(null); @@ -69,6 +71,9 @@ type Props = { /** Function that is given to upload mutation, handling the sending of file to API */ uploadFunction: (file: File) => Promise; onUpload?: (isUploading: boolean) => void; + fileDownLoadFunction?: FileDownLoadFunction; + fileDeleteFunction: FileDeleteFunction; + onFileDelete?: () => void; }; export default function FileUpload({ @@ -81,6 +86,9 @@ export default function FileUpload({ existingAttachments = [], uploadFunction, onUpload, + fileDownLoadFunction, + fileDeleteFunction, + onFileDelete, }: Props) { const { t } = useTranslation(); const locale = useLocale(); @@ -160,6 +168,13 @@ export default function FileUpload({ totalCount={newFiles.length} /> )} + + ); } diff --git a/src/common/components/fileUpload/types.ts b/src/common/components/fileUpload/types.ts new file mode 100644 index 000000000..fb42ff241 --- /dev/null +++ b/src/common/components/fileUpload/types.ts @@ -0,0 +1,5 @@ +import { AttachmentMetadata } from '../../types/attachment'; + +export type FileDownLoadFunction = (file: AttachmentMetadata) => Promise; + +export type FileDeleteFunction = (file: AttachmentMetadata | null) => Promise; diff --git a/src/locales/fi.json b/src/locales/fi.json index a305ed34c..80272ec05 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -110,7 +110,8 @@ "dragAttachments": "Raahaa tiedostot tänne", "additionalInfo": "Lisätiedot", "added": "Lisätty", - "addedFiles": "Lisätyt liitetiedostot" + "addedFiles": "Lisätyt liitetiedostot", + "today": "tänään" }, "dialog": { "titles": {