Skip to content

Commit

Permalink
wip HAI-2066 File list for upload component
Browse files Browse the repository at this point in the history
  • Loading branch information
markohaarni committed Oct 27, 2023
1 parent d351dda commit 3f58bab
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 6 deletions.
23 changes: 18 additions & 5 deletions src/common/components/fileDownloadLink/FileDownloadLink.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -10,16 +10,30 @@ type Props = {
queryKey: QueryKey;
queryFunction: QueryFunction<string>;
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('');
const linkRef = useRef<HTMLAnchorElement>(null);
const [errorText, setErrorText] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (fileUrl) {
linkRef.current?.click();
}
}, [fileUrl]);

async function fetchFile(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
setErrorText(null);
const cachedUrl = queryClient.getQueryData<string>(queryKey);
Expand All @@ -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'));
Expand All @@ -49,7 +62,7 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco

if (loading) {
return (
<Box display="flex" alignItems="center">
<Box display="flex" alignItems="center" width="max-content">
<LoadingSpinner small />
</Box>
);
Expand All @@ -59,7 +72,7 @@ function FileDownloadLink({ linkText, fileName, queryKey, queryFunction, linkIco
<>
<Link href={fileUrl} download={fileName} onClick={fetchFile} ref={linkRef}>
{linkIcon}
{linkText}
<span className={linkTextStyles}>{linkText}</span>
</Link>

{errorText && (
Expand Down
102 changes: 102 additions & 0 deletions src/common/components/fileUpload/FileList.tsx
Original file line number Diff line number Diff line change
@@ -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<AttachmentMetadata | null>(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 (
<>
<Box as="ul" tabIndex={-1} marginTop="var(--spacing-m)">
{files.map((file) => {
return (
<FileListItem
fileMetadata={file}
key={file.id}
onDeleteFile={handleFileDelete}
fileDownLoadFunction={fileDownLoadFunction}
deletingFile={fileToDelete?.id === file.id && deleteMutation.isLoading}
/>
);
})}
</Box>

{/* File remove dialog */}
<ConfirmationDialog
title={t('form:dialog:titles:removeAttachment')}
description={t('form:dialog:descriptions:removeAttachment', {
fileName: fileToDelete?.fileName,
})}
isOpen={showFileDeleteDialog}
close={closeFileDeleteDialog}
mainAction={confirmFileDelete}
mainBtnLabel={t('common:buttons:remove')}
mainBtnIcon={<IconTrash aria-hidden />}
variant="danger"
/>

{/* Attachment delete success notification */}
{deleteMutation.isSuccess && (
<Notification
position="top-right"
dismissible
autoClose
autoCloseDuration={3000}
type="success"
label={t('form:notifications:labels:attachmentRemoved')}
closeButtonLabelText={t('common:components:notification:closeButtonLabelText')}
onClose={closeDeleteSuccessNotification}
>
{t('form:notifications:descriptions:attachmentRemoved', {
fileName: fileToDelete?.fileName,
})}
</Notification>
)}
</>
);
}
33 changes: 33 additions & 0 deletions src/common/components/fileUpload/FileListItem.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
80 changes: 80 additions & 0 deletions src/common/components/fileUpload/FileListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex
as="li"
tabIndex={-1}
className={styles.fileListItem}
alignItems="flex-start"
gridColumnGap="var(--spacing-xs)"
gridRowGap="var(--spacing-2-xs)"
mb="var(--spacing-xs)"
>
<IconDocument aria-hidden />
<Flex wrap="nowrap" flex="1" alignItems="flex-start">
{fileDownLoadFunction === undefined ? (
<Text tag="p" className={styles.fileListItemName}>
{fileMetadata.fileName}
</Text>
) : (
<>
<FileDownloadLink
linkText={fileMetadata.fileName}
fileName={fileMetadata.fileName}
linkTextStyles={styles.fileListItemName}
queryKey={['attachmentContent', fileMetadata.id]}
queryFunction={() => fileDownLoadFunction(fileMetadata)}
/>
<IconDownload aria-hidden className={styles.fileListItemDownloadIcon} />
</>
)}
</Flex>
<Box as="p" color="var(--color-black-60)" className="text-sm">
{t('form:labels:added')} {dateAddedText}
</Box>
{showDeleteButton && (
<Button
className={styles.fileListItemButton}
iconLeft={<IconCross aria-hidden />}
variant="supplementary"
size="small"
theme="black"
onClick={() => onDeleteFile(fileMetadata)}
data-testid={`delete-${fileMetadata.id}`}
isLoading={deletingFile}
>
{t('common:buttons:remove')}
</Button>
)}
</Flex>
);
}
4 changes: 4 additions & 0 deletions src/common/components/fileUpload/FileUpload.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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'), {
Expand Down
15 changes: 15 additions & 0 deletions src/common/components/fileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null);
Expand Down Expand Up @@ -69,6 +71,9 @@ type Props<T extends AttachmentMetadata> = {
/** Function that is given to upload mutation, handling the sending of file to API */
uploadFunction: (file: File) => Promise<T>;
onUpload?: (isUploading: boolean) => void;
fileDownLoadFunction?: FileDownLoadFunction;
fileDeleteFunction: FileDeleteFunction;
onFileDelete?: () => void;
};

export default function FileUpload<T extends AttachmentMetadata>({
Expand All @@ -81,6 +86,9 @@ export default function FileUpload<T extends AttachmentMetadata>({
existingAttachments = [],
uploadFunction,
onUpload,
fileDownLoadFunction,
fileDeleteFunction,
onFileDelete,
}: Props<T>) {
const { t } = useTranslation();
const locale = useLocale();
Expand Down Expand Up @@ -160,6 +168,13 @@ export default function FileUpload<T extends AttachmentMetadata>({
totalCount={newFiles.length}
/>
)}

<FileList
files={existingAttachments}
fileDownLoadFunction={fileDownLoadFunction}
fileDeleteFunction={fileDeleteFunction}
onFileDelete={onFileDelete}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions src/common/components/fileUpload/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AttachmentMetadata } from '../../types/attachment';

export type FileDownLoadFunction = (file: AttachmentMetadata) => Promise<string>;

export type FileDeleteFunction = (file: AttachmentMetadata | null) => Promise<void>;
3 changes: 2 additions & 1 deletion src/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit 3f58bab

Please sign in to comment.