From b4ca29494539194d283f2a4a1b7973007525ecf2 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Tue, 24 Oct 2023 09:04:38 +0300 Subject: [PATCH 1/4] wip HAI-2065 Implement file upload component --- .../fileUpload/FileUpload.module.scss | 16 ++ .../components/fileUpload/FileUpload.tsx | 168 ++++++++++++++++++ src/common/components/fileUpload/utils.ts | 11 ++ src/common/types/attachment.ts | 6 + src/domain/application/attachments.test.ts | 2 +- src/domain/application/attachments.ts | 16 +- src/domain/application/types/application.ts | 9 +- .../johtoselvitys/JohtoselvitysContainer.tsx | 3 +- src/locales/fi.json | 4 + 9 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 src/common/components/fileUpload/FileUpload.module.scss create mode 100644 src/common/components/fileUpload/FileUpload.tsx create mode 100644 src/common/components/fileUpload/utils.ts create mode 100644 src/common/types/attachment.ts diff --git a/src/common/components/fileUpload/FileUpload.module.scss b/src/common/components/fileUpload/FileUpload.module.scss new file mode 100644 index 000000000..df0616d09 --- /dev/null +++ b/src/common/components/fileUpload/FileUpload.module.scss @@ -0,0 +1,16 @@ +.uploadContainer { + min-height: 250px; + margin-bottom: var(--spacing-s); +} + +.loadingContainer { + margin-left: var(--spacing-m); + + .loadingSpinner { + margin-right: var(--spacing-s); + } + + .loadingText { + font-weight: 500; + } +} diff --git a/src/common/components/fileUpload/FileUpload.tsx b/src/common/components/fileUpload/FileUpload.tsx new file mode 100644 index 000000000..59bfa782e --- /dev/null +++ b/src/common/components/fileUpload/FileUpload.tsx @@ -0,0 +1,168 @@ +import { useEffect, useRef, useState } from 'react'; +import { QueryKey, useMutation, useQueryClient } from 'react-query'; +import { FileInput, IconCheckCircleFill, LoadingSpinner } from 'hds-react'; +import { useTranslation } from 'react-i18next'; +import { differenceBy } from 'lodash'; +import { AxiosError } from 'axios'; +import useLocale from '../../hooks/useLocale'; +import { AttachmentMetadata } from '../../types/attachment'; +import { Flex } from '@chakra-ui/react'; +import Text from '../text/Text'; +import styles from './FileUpload.module.scss'; +import { removeDuplicateAttachments } from './utils'; + +function useDroppedFiles() { + const ref = useRef(null); + const droppedFiles = useRef([]); + + useEffect(() => { + function dropHandler(ev: DragEvent) { + if (ev.dataTransfer) { + droppedFiles.current = Array.from(ev.dataTransfer.files); + } + } + + if (ref.current) { + ref.current.ondrop = dropHandler; + } + }, []); + + return { ref, droppedFiles }; +} + +function SuccessNotification({ + successfulCount, + totalCount, +}: { + successfulCount: number; + totalCount: number; +}) { + const { t } = useTranslation(); + + return ( + + +

+ {t('common:components:fileUpload:successNotification', { + successful: successfulCount, + total: totalCount, + })} +

+
+ ); +} + +// TODO: Kutsujen perumisen voisi tehdä omassa +// subtaskissaan, se on vielä mysteeri miten tehdään + +type Props = { + fileInputId: string; + accept: string | undefined; + maxSize: number | undefined; + dragAndDrop: boolean | undefined; + multiple: boolean | undefined; + queryKey: QueryKey; + existingAttachments?: T[]; + uploadFunction: (file: File) => Promise; + onUpload?: (isUploading: boolean) => void; +}; + +export default function FileUpload({ + fileInputId, + accept, + maxSize, + dragAndDrop, + multiple, + queryKey, + existingAttachments = [], + uploadFunction, + onUpload, +}: Props) { + const { t } = useTranslation(); + const locale = useLocale(); + const queryClient = useQueryClient(); + const [newFiles, setNewFiles] = useState([]); + const [invalidFiles, setInvalidFiles] = useState([]); + const uploadMutation = useMutation(uploadFunction, { + onError(error: AxiosError, file) { + // TODO: Invalid files pitäisi ehkä olla array of objects, + // joissa olisi virheen syy ja tiedosto, + // tai niin, että se on array of strings, joissa jokainen item on se + // error teksti + setInvalidFiles((files) => [...files, file]); + }, + }); + const [filesUploading, setFilesUploading] = useState(false); + const { ref: dropZoneRef, droppedFiles } = useDroppedFiles(); + + async function uploadFiles(files: File[]) { + setFilesUploading(true); + if (onUpload) { + onUpload(true); + } + + const mutations = files.map((file) => { + return uploadMutation.mutateAsync(file); + }); + + await Promise.allSettled(mutations); + + setFilesUploading(false); + queryClient.invalidateQueries(queryKey); + if (onUpload) { + onUpload(false); + } + } + + function handleFilesChange(files: File[]) { + // Filter out attachments that have same names as those that have already been sent + const filesToSend = removeDuplicateAttachments(files, existingAttachments); + + // Determine which files haven't passed HDS FileInput validation by comparing + // files in input element or files dropped into drop zone to files received as + // argument to this onChange function + const inputElem = document.getElementById(fileInputId) as HTMLInputElement; + const inputElemFiles = inputElem.files ? Array.from(inputElem.files) : []; + const allFiles = inputElemFiles.length > 0 ? inputElemFiles : droppedFiles.current; + const invalidFilesArr = differenceBy(allFiles, filesToSend, 'name'); + + setNewFiles(allFiles); + setInvalidFiles(invalidFilesArr); + uploadFiles(filesToSend); + } + + return ( +
+ + {filesUploading ? ( + + + + {t('common:components:fileUpload:loadingText')} + + + ) : ( + + )} + + + {!filesUploading && newFiles.length > 0 && ( + + )} +
+ ); +} diff --git a/src/common/components/fileUpload/utils.ts b/src/common/components/fileUpload/utils.ts new file mode 100644 index 000000000..1e0f231ae --- /dev/null +++ b/src/common/components/fileUpload/utils.ts @@ -0,0 +1,11 @@ +import { AttachmentMetadata } from '../../types/attachment'; + +// Filter out duplicate files based on file name +export function removeDuplicateAttachments( + files: File[], + attachments: T[] | undefined, +): File[] { + return files.filter( + (file) => attachments?.every((attachment) => attachment.fileName !== file.name), + ); +} diff --git a/src/common/types/attachment.ts b/src/common/types/attachment.ts new file mode 100644 index 000000000..73ab3075e --- /dev/null +++ b/src/common/types/attachment.ts @@ -0,0 +1,6 @@ +export type AttachmentMetadata = { + id: string; + fileName: string; + createdByUserId: string; + createdAt: string; +}; diff --git a/src/domain/application/attachments.test.ts b/src/domain/application/attachments.test.ts index 8b4fbced1..4b047d85c 100644 --- a/src/domain/application/attachments.test.ts +++ b/src/domain/application/attachments.test.ts @@ -1,4 +1,4 @@ -import { removeDuplicateAttachments } from './attachments'; +import { removeDuplicateAttachments } from '../../common/components/fileUpload/utils'; import { ApplicationAttachmentMetadata } from './types/application'; const attachments: ApplicationAttachmentMetadata[] = [ diff --git a/src/domain/application/attachments.ts b/src/domain/application/attachments.ts index 827811c0f..158916c80 100644 --- a/src/domain/application/attachments.ts +++ b/src/domain/application/attachments.ts @@ -4,7 +4,7 @@ import { ApplicationAttachmentMetadata, AttachmentType } from './types/applicati // Get attachments metadata related to an application export async function getAttachments(applicationId: number | null | undefined) { const { data } = await api.get( - `/hakemukset/${applicationId}/liitteet` + `/hakemukset/${applicationId}/liitteet`, ); return data; } @@ -26,7 +26,7 @@ export async function uploadAttachment({ headers: { 'Content-Type': 'multipart/form-data', }, - } + }, ); return data; } @@ -35,7 +35,7 @@ export async function uploadAttachment({ export async function getAttachmentFile(applicationId: number, attachmentId: string) { const { data } = await api.get( `/hakemukset/${applicationId}/liitteet/${attachmentId}/content`, - { responseType: 'blob' } + { responseType: 'blob' }, ); return URL.createObjectURL(data); } @@ -50,13 +50,3 @@ export async function deleteAttachment({ }) { await api.delete(`/hakemukset/${applicationId}/liitteet/${attachmentId}`); } - -// Filter out duplicate files based on file name -export function removeDuplicateAttachments( - files: File[], - attachments: ApplicationAttachmentMetadata[] | undefined -) { - return files.filter((file) => - attachments?.every((attachment) => attachment.fileName !== file.name) - ); -} diff --git a/src/domain/application/types/application.ts b/src/domain/application/types/application.ts index f6ddfc931..fa67f460f 100644 --- a/src/domain/application/types/application.ts +++ b/src/domain/application/types/application.ts @@ -1,3 +1,4 @@ +import { AttachmentMetadata } from './../../../common/types/attachment'; import { Polygon, Position } from 'geojson'; import { Coordinate } from 'ol/coordinate'; import { CRS } from '../../../common/types/hanke'; @@ -100,14 +101,10 @@ export type ApplicationArea = { export type AttachmentType = 'MUU' | 'LIIKENNEJARJESTELY' | 'VALTAKIRJA'; -export type ApplicationAttachmentMetadata = { - id: string; - fileName: string; - createdByUserId: string; - createdAt: string; +export interface ApplicationAttachmentMetadata extends AttachmentMetadata { applicationId: number; attachmentType: AttachmentType; -}; +} export type JohtoselvitysData = { applicationType: ApplicationType; diff --git a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx index 7883019ba..7343c5cfe 100644 --- a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx +++ b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx @@ -34,9 +34,10 @@ import useHanke from '../hanke/hooks/useHanke'; import { AlluStatus, Application } from '../application/types/application'; import Attachments from './Attachments'; import ConfirmationDialog from '../../common/components/HDSConfirmationDialog/ConfirmationDialog'; -import { removeDuplicateAttachments, uploadAttachment } from '../application/attachments'; +import { uploadAttachment } from '../application/attachments'; import useAttachments from '../application/hooks/useAttachments'; import { APPLICATION_ID_STORAGE_KEY } from '../application/constants'; +import { removeDuplicateAttachments } from '../../common/components/fileUpload/utils'; type Props = { hankeData?: HankeData; diff --git a/src/locales/fi.json b/src/locales/fi.json index 259fe8b08..ce0597c4d 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -48,6 +48,10 @@ "errorLoadingInfo": { "textTop": "Virhe tietojen lataamisessa.", "textBottom": "Yritä hetken päästä uudelleen." + }, + "fileUpload": { + "loadingText": "Tallennetaan tiedostoja", + "successNotification": "{{successful}}/{{total}} tiedostoa tallennettu" } }, "error": "Tapahtui virhe. Yritä uudestaan.", From d351dda44b2a0b946e1381fe325adc6364d90675 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Wed, 25 Oct 2023 14:02:15 +0300 Subject: [PATCH 2/4] HAI-2065 Implement file upload component File upload component uses HDS FileInput internally to handle user adding files from their machine and immediately sends added files to backend and shows a loading spinner and loading text while upload is in process. --- .../components/fileUpload/FileUpload.test.tsx | 127 ++++++++++++++++++ .../components/fileUpload/FileUpload.tsx | 59 ++++---- src/common/components/fileUpload/utils.ts | 8 +- src/domain/application/attachments.test.ts | 27 ++-- .../johtoselvitys/JohtoselvitysContainer.tsx | 2 +- src/domain/mocks/handlers.ts | 4 + src/locales/fi.json | 2 +- src/testUtils/render.tsx | 10 +- 8 files changed, 194 insertions(+), 45 deletions(-) create mode 100644 src/common/components/fileUpload/FileUpload.test.tsx diff --git a/src/common/components/fileUpload/FileUpload.test.tsx b/src/common/components/fileUpload/FileUpload.test.tsx new file mode 100644 index 000000000..401c71364 --- /dev/null +++ b/src/common/components/fileUpload/FileUpload.test.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { rest } from 'msw'; +import { act, fireEvent, render, screen, waitFor } from '../../../testUtils/render'; +import api from '../../../domain/api/api'; +import FileUpload from './FileUpload'; +import { server } from '../../../domain/mocks/test-server'; + +async function uploadAttachment({ id, file }: { id: number; file: File }) { + const { data } = await api.post(`/hakemukset/${id}/liitteet`, { + liite: file, + }); + return data; +} + +function uploadFunction(file: File) { + return uploadAttachment({ + id: 1, + file, + }); +} + +test('Should upload files successfully and loading indicator is displayed', async () => { + server.use( + rest.post('/api/hakemukset/:id/liitteet', async (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200)); + }), + ); + + const inputLabel = 'Choose a file'; + const { user } = render( + , + ); + const fileUpload = screen.getByLabelText(inputLabel); + user.upload(fileUpload, [ + new File(['test-a'], 'test-file-a.png', { type: 'image/png' }), + new File(['test-b'], 'test-file-b.jpg', { type: 'image/jpg' }), + ]); + + await waitFor(() => screen.findByText('Tallennetaan tiedostoja')); + await act(async () => { + waitFor(() => expect(screen.queryByText('Tallennetaan tiedostoja')).not.toBeInTheDocument()); + }); + await waitFor(() => { + expect(screen.queryByText('2/2 tiedosto(a) tallennettu')).toBeInTheDocument(); + }); +}); + +test('Should show amount of successful files uploaded correctly when file fails in validation', async () => { + const inputLabel = 'Choose a file'; + const { user } = render( + , + undefined, + undefined, + { applyAccept: false }, + ); + const fileUpload = screen.getByLabelText(inputLabel); + user.upload(fileUpload, [ + new File(['test-a'], 'test-file-a.png', { type: 'image/png' }), + new File(['test-b'], 'test-file-b.pdf', { type: 'application/pdf' }), + ]); + + await waitFor(() => { + expect(screen.queryByText('1/2 tiedosto(a) tallennettu')).toBeInTheDocument(); + }); +}); + +test('Should show amount of successful files uploaded correctly when request fails', async () => { + server.use( + rest.post('/api/hakemukset/:id/liitteet', async (req, res, ctx) => { + return res(ctx.status(400), ctx.json({ errorMessage: 'Failed for testing purposes' })); + }), + ); + + const inputLabel = 'Choose a file'; + const { user } = render( + , + ); + const fileUpload = screen.getByLabelText(inputLabel); + user.upload(fileUpload, [new File(['test-a'], 'test-file-a.png', { type: 'image/png' })]); + + await waitFor(() => { + expect(screen.queryByText('0/1 tiedosto(a) tallennettu')).toBeInTheDocument(); + }); +}); + +test('Should upload files when user drops them into drag-and-drop area', async () => { + const inputLabel = 'Choose files'; + const file = new File(['test-file'], 'test-file-a', { type: 'image/png' }); + const file2 = new File(['test-file'], 'test-file-b', { type: 'image/png' }); + const file3 = new File(['test-file'], 'test-file-c', { type: 'image/png' }); + render( + , + ); + fireEvent.drop(screen.getByText('Raahaa tiedostot tänne'), { + dataTransfer: { + files: [file, file2, file3], + }, + }); + + await waitFor(() => { + expect(screen.queryByText('3/3 tiedosto(a) tallennettu')).toBeInTheDocument(); + }); +}); diff --git a/src/common/components/fileUpload/FileUpload.tsx b/src/common/components/fileUpload/FileUpload.tsx index 59bfa782e..9abf96652 100644 --- a/src/common/components/fileUpload/FileUpload.tsx +++ b/src/common/components/fileUpload/FileUpload.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { QueryKey, useMutation, useQueryClient } from 'react-query'; +import { useMutation } from 'react-query'; import { FileInput, IconCheckCircleFill, LoadingSpinner } from 'hds-react'; import { useTranslation } from 'react-i18next'; import { differenceBy } from 'lodash'; @@ -11,14 +11,14 @@ import Text from '../text/Text'; import styles from './FileUpload.module.scss'; import { removeDuplicateAttachments } from './utils'; -function useDroppedFiles() { +function useDragAndDropFiles() { const ref = useRef(null); - const droppedFiles = useRef([]); + const files = useRef([]); useEffect(() => { function dropHandler(ev: DragEvent) { if (ev.dataTransfer) { - droppedFiles.current = Array.from(ev.dataTransfer.files); + files.current = Array.from(ev.dataTransfer.files); } } @@ -27,7 +27,7 @@ function useDroppedFiles() { } }, []); - return { ref, droppedFiles }; + return { ref, files }; } function SuccessNotification({ @@ -52,48 +52,47 @@ function SuccessNotification({ ); } -// TODO: Kutsujen perumisen voisi tehdä omassa -// subtaskissaan, se on vielä mysteeri miten tehdään - type Props = { - fileInputId: string; - accept: string | undefined; - maxSize: number | undefined; - dragAndDrop: boolean | undefined; - multiple: boolean | undefined; - queryKey: QueryKey; + /** id of the input element */ + id: string; + /** Label for the input */ + label?: string; + /** A comma-separated list of unique file type specifiers describing file types to allow. */ + accept?: string; + /** Maximum file size in bytes. */ + maxSize?: number; + /** If true, the file input will have a drag and drop area */ + dragAndDrop?: boolean; + /** A Boolean that indicates that more than one file can be chosen */ + multiple?: boolean; existingAttachments?: T[]; + /** Function that is given to upload mutation, handling the sending of file to API */ uploadFunction: (file: File) => Promise; onUpload?: (isUploading: boolean) => void; }; export default function FileUpload({ - fileInputId, + id, + label, accept, maxSize, dragAndDrop, multiple, - queryKey, existingAttachments = [], uploadFunction, onUpload, }: Props) { const { t } = useTranslation(); const locale = useLocale(); - const queryClient = useQueryClient(); const [newFiles, setNewFiles] = useState([]); const [invalidFiles, setInvalidFiles] = useState([]); const uploadMutation = useMutation(uploadFunction, { onError(error: AxiosError, file) { - // TODO: Invalid files pitäisi ehkä olla array of objects, - // joissa olisi virheen syy ja tiedosto, - // tai niin, että se on array of strings, joissa jokainen item on se - // error teksti setInvalidFiles((files) => [...files, file]); }, }); const [filesUploading, setFilesUploading] = useState(false); - const { ref: dropZoneRef, droppedFiles } = useDroppedFiles(); + const { ref: dropZoneRef, files: dragAndDropFiles } = useDragAndDropFiles(); async function uploadFiles(files: File[]) { setFilesUploading(true); @@ -108,7 +107,6 @@ export default function FileUpload({ await Promise.allSettled(mutations); setFilesUploading(false); - queryClient.invalidateQueries(queryKey); if (onUpload) { onUpload(false); } @@ -116,19 +114,19 @@ export default function FileUpload({ function handleFilesChange(files: File[]) { // Filter out attachments that have same names as those that have already been sent - const filesToSend = removeDuplicateAttachments(files, existingAttachments); + const [filesToUpload] = removeDuplicateAttachments(files, existingAttachments); // Determine which files haven't passed HDS FileInput validation by comparing // files in input element or files dropped into drop zone to files received as // argument to this onChange function - const inputElem = document.getElementById(fileInputId) as HTMLInputElement; + const inputElem = document.getElementById(id) as HTMLInputElement; const inputElemFiles = inputElem.files ? Array.from(inputElem.files) : []; - const allFiles = inputElemFiles.length > 0 ? inputElemFiles : droppedFiles.current; - const invalidFilesArr = differenceBy(allFiles, filesToSend, 'name'); + const allFiles = inputElemFiles.length > 0 ? inputElemFiles : dragAndDropFiles.current; + const invalidFilesArr = differenceBy(allFiles, filesToUpload, 'name'); setNewFiles(allFiles); setInvalidFiles(invalidFilesArr); - uploadFiles(filesToSend); + uploadFiles(filesToUpload); } return ( @@ -143,16 +141,15 @@ export default function FileUpload({ ) : ( )} diff --git a/src/common/components/fileUpload/utils.ts b/src/common/components/fileUpload/utils.ts index 1e0f231ae..906d96a41 100644 --- a/src/common/components/fileUpload/utils.ts +++ b/src/common/components/fileUpload/utils.ts @@ -4,8 +4,12 @@ import { AttachmentMetadata } from '../../types/attachment'; export function removeDuplicateAttachments( files: File[], attachments: T[] | undefined, -): File[] { - return files.filter( +): [File[], File[]] { + const duplicateFiles = files.filter( + (file) => attachments?.some((attachment) => attachment.fileName === file.name), + ); + const newFiles = files.filter( (file) => attachments?.every((attachment) => attachment.fileName !== file.name), ); + return [newFiles, duplicateFiles]; } diff --git a/src/domain/application/attachments.test.ts b/src/domain/application/attachments.test.ts index 4b047d85c..ecff969f9 100644 --- a/src/domain/application/attachments.test.ts +++ b/src/domain/application/attachments.test.ts @@ -47,8 +47,13 @@ test('Should filter out existing attachments', () => { ]; expect(removeDuplicateAttachments(files, attachments)).toMatchObject([ - { name: 'Haitaton_liite_uusi.pdf' }, - { name: 'Haitaton_liite_uusi_2.jpg' }, + [{ name: 'Haitaton_liite_uusi.pdf' }, { name: 'Haitaton_liite_uusi_2.jpg' }], + [ + { name: 'Haitaton_liite.png' }, + { name: 'Haitaton_liite_2.txt' }, + { name: 'Haitaton_liite_3.jpg' }, + { name: 'Haitaton_liite_4.pdf' }, + ], ]); }); @@ -60,14 +65,20 @@ test('Should not filter out anything from only new files', () => { ]; expect(removeDuplicateAttachments(files, attachments)).toMatchObject([ - { name: 'Haitaton_liite_uusi.pdf' }, - { name: 'Haitaton_liite_uusi_2.jpg' }, - { name: 'Haitaton_liite_uusi_3.jpg' }, + [ + { name: 'Haitaton_liite_uusi.pdf' }, + { name: 'Haitaton_liite_uusi_2.jpg' }, + { name: 'Haitaton_liite_uusi_3.jpg' }, + ], + [], ]); expect(removeDuplicateAttachments(files, [])).toMatchObject([ - { name: 'Haitaton_liite_uusi.pdf' }, - { name: 'Haitaton_liite_uusi_2.jpg' }, - { name: 'Haitaton_liite_uusi_3.jpg' }, + [ + { name: 'Haitaton_liite_uusi.pdf' }, + { name: 'Haitaton_liite_uusi_2.jpg' }, + { name: 'Haitaton_liite_uusi_3.jpg' }, + ], + [], ]); }); diff --git a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx index 7343c5cfe..ce45fd6cb 100644 --- a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx +++ b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx @@ -290,7 +290,7 @@ const JohtoselvitysContainer: React.FC> = ({ } // Filter out attachments that have same names as those that have already been sent - const filesToSend = removeDuplicateAttachments(newAttachments, existingAttachments); + const [filesToSend] = removeDuplicateAttachments(newAttachments, existingAttachments); const mutations = filesToSend.map((file) => attachmentUploadMutation.mutateAsync({ diff --git a/src/domain/mocks/handlers.ts b/src/domain/mocks/handlers.ts index 4a3408ad7..3143121f5 100644 --- a/src/domain/mocks/handlers.ts +++ b/src/domain/mocks/handlers.ts @@ -233,4 +233,8 @@ export const handlers = [ rest.post(`${apiUrl}/kayttajat/:kayttajaId/kutsu`, async (req, res, ctx) => { return res(ctx.delay(), ctx.status(204)); }), + + rest.post(`${apiUrl}/hakemukset/:id/liitteet`, async (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200)); + }), ]; diff --git a/src/locales/fi.json b/src/locales/fi.json index ce0597c4d..a305ed34c 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -51,7 +51,7 @@ }, "fileUpload": { "loadingText": "Tallennetaan tiedostoja", - "successNotification": "{{successful}}/{{total}} tiedostoa tallennettu" + "successNotification": "{{successful}}/{{total}} tiedosto(a) tallennettu" } }, "error": "Tapahtui virhe. Yritä uudestaan.", diff --git a/src/testUtils/render.tsx b/src/testUtils/render.tsx index 30d075783..8a71b3fbf 100644 --- a/src/testUtils/render.tsx +++ b/src/testUtils/render.tsx @@ -42,11 +42,17 @@ const AllTheProviders = ({ children }: Props) => { ); }; -const customRender = (ui: React.ReactElement, options: RenderOptions = {}, route = '/') => { +const customRender = ( + ui: React.ReactElement, + options: RenderOptions = {}, + route = '/', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + userEventOptions?: any, +) => { window.history.pushState({}, 'Test page', route); window.scrollTo = function () {}; return { - user: userEvent.setup(), + user: userEvent.setup(userEventOptions), ...render(ui, { wrapper: AllTheProviders as React.ComponentType>, ...options, From 6cc53907b8da32e0bbd5885cb7b0efd8c889f200 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Wed, 1 Nov 2023 13:00:37 +0200 Subject: [PATCH 3/4] fixup! HAI-2065 Implement file upload component --- src/common/components/fileUpload/utils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/components/fileUpload/utils.ts b/src/common/components/fileUpload/utils.ts index 906d96a41..2d0fa1236 100644 --- a/src/common/components/fileUpload/utils.ts +++ b/src/common/components/fileUpload/utils.ts @@ -2,14 +2,14 @@ import { AttachmentMetadata } from '../../types/attachment'; // Filter out duplicate files based on file name export function removeDuplicateAttachments( - files: File[], - attachments: T[] | undefined, + addedFiles: File[], + existingAttachments: T[] | undefined, ): [File[], File[]] { - const duplicateFiles = files.filter( - (file) => attachments?.some((attachment) => attachment.fileName === file.name), + const duplicateFiles = addedFiles.filter( + (file) => existingAttachments?.some((attachment) => attachment.fileName === file.name), ); - const newFiles = files.filter( - (file) => attachments?.every((attachment) => attachment.fileName !== file.name), + const newFiles = addedFiles.filter( + (file) => existingAttachments?.every((attachment) => attachment.fileName !== file.name), ); return [newFiles, duplicateFiles]; } From 5c729ca5a024a59606f411f6a2a8035c99264084 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Wed, 1 Nov 2023 16:22:52 +0200 Subject: [PATCH 4/4] fixup! HAI-2065 Implement file upload component --- src/common/components/fileUpload/FileUpload.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/components/fileUpload/FileUpload.tsx b/src/common/components/fileUpload/FileUpload.tsx index 9abf96652..7368bcf54 100644 --- a/src/common/components/fileUpload/FileUpload.tsx +++ b/src/common/components/fileUpload/FileUpload.tsx @@ -33,10 +33,10 @@ function useDragAndDropFiles() { function SuccessNotification({ successfulCount, totalCount, -}: { +}: Readonly<{ successfulCount: number; totalCount: number; -}) { +}>) { const { t } = useTranslation(); return ( @@ -81,7 +81,7 @@ export default function FileUpload({ existingAttachments = [], uploadFunction, onUpload, -}: Props) { +}: Readonly>) { const { t } = useTranslation(); const locale = useLocale(); const [newFiles, setNewFiles] = useState([]);