From 12242848b08bfb269112c8804a76e032ea239abd Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sun, 24 Jul 2022 23:18:32 +0200 Subject: [PATCH] File upload status (v2 backport) (#4219) --- .../server/files/FileManagerContainer.tsx | 2 + .../server/files/FileManagerStatus.tsx | 72 +++++++++++++++ .../components/server/files/UploadButton.tsx | 88 +++++++++++++------ resources/scripts/state/server/files.ts | 19 ++++ 4 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 resources/scripts/components/server/files/FileManagerStatus.tsx diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 756a403a4..83440aecc 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -13,6 +13,7 @@ import tw from 'twin.macro'; import { Button } from '@/components/elements/button/index'; import { ServerContext } from '@/state/server'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; +import FileManagerStatus from '@/components/server/files/FileManagerStatus'; import MassActionsBar from '@/components/server/files/MassActionsBar'; import UploadButton from '@/components/server/files/UploadButton'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; @@ -104,6 +105,7 @@ export default () => { ))} + )} diff --git a/resources/scripts/components/server/files/FileManagerStatus.tsx b/resources/scripts/components/server/files/FileManagerStatus.tsx new file mode 100644 index 000000000..a286c4155 --- /dev/null +++ b/resources/scripts/components/server/files/FileManagerStatus.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import tw, { styled } from 'twin.macro'; +import { ServerContext } from '@/state/server'; +import { bytesToString } from '@/lib/formatters'; + +const SpinnerCircle = styled.circle` + transition: stroke-dashoffset 0.35s; + transform: rotate(-90deg); + transform-origin: 50% 50%; +`; + +function Spinner({ progress }: { progress: number }) { + const stroke = 3; + const radius = 20; + const normalizedRadius = radius - stroke * 2; + const circumference = normalizedRadius * 2 * Math.PI; + + return ( + + + + + ); +} + +function FileManagerStatus() { + const uploads = ServerContext.useStoreState((state) => state.files.uploads); + + return ( +
+ {uploads.length > 0 && ( +
+ {uploads + .sort((a, b) => a.total - b.total) + .map((f) => ( +
+
+ +
+ +
+ + {f.name} ({bytesToString(f.loaded)}/{bytesToString(f.total)}) + +
+
+ ))} +
+ )} +
+ ); +} + +export default FileManagerStatus; diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index db566f4ba..7b1d7c8a1 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -7,7 +7,6 @@ import styled from 'styled-components/macro'; import { ModalMask } from '@/components/elements/Modal'; import Fade from '@/components/elements/Fade'; import useEventListener from '@/plugins/useEventListener'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import useFlash from '@/plugins/useFlash'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import { ServerContext } from '@/state/server'; @@ -19,18 +18,40 @@ const InnerContainer = styled.div` ${tw`bg-black w-full border-4 border-primary-500 border-dashed rounded p-10 mx-10`} `; +function isFileOrDirectory(event: DragEvent): boolean { + if (!event.dataTransfer?.types) { + return false; + } + + for (let i = 0; i < event.dataTransfer.types.length; i++) { + // Check if the item being dragged is not a file. + // On Firefox a file of type "application/x-moz-file" is also in the array. + if (event.dataTransfer.types[i] !== 'Files' && event.dataTransfer.types[i] !== 'application/x-moz-file') { + return false; + } + } + + return true; +} + export default ({ className }: WithClassname) => { const fileUploadInput = useRef(null); - const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const [timeouts, setTimeouts] = useState([]); const [visible, setVisible] = useState(false); - const [loading, setLoading] = useState(false); const { mutate } = useFileManagerSwr(); const { clearFlashes, clearAndAddHttpError } = useFlash(); + + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); const directory = ServerContext.useStoreState((state) => state.files.directory); + const appendFileUpload = ServerContext.useStoreActions((actions) => actions.files.appendFileUpload); + const removeFileUpload = ServerContext.useStoreActions((actions) => actions.files.removeFileUpload); useEventListener( 'dragenter', (e) => { + if (!isFileOrDirectory(e)) { + return; + } e.stopPropagation(); setVisible(true); }, @@ -40,6 +61,9 @@ export default ({ className }: WithClassname) => { useEventListener( 'dragexit', (e) => { + if (!isFileOrDirectory(e)) { + return; + } e.stopPropagation(); setVisible(false); }, @@ -57,27 +81,47 @@ export default ({ className }: WithClassname) => { }; }, [visible]); - const onFileSubmission = (files: FileList) => { - const form = new FormData(); - Array.from(files).forEach((file) => form.append('files', file)); + useEffect(() => { + return () => timeouts.forEach(clearTimeout); + }, []); - setLoading(true); + const onFileSubmission = (files: FileList) => { + const formData: FormData[] = []; + Array.from(files).forEach((file) => { + const form = new FormData(); + form.append('files', file); + formData.push(form); + }); clearFlashes('files'); - getFileUploadUrl(uuid) - .then((url) => - axios.post(`${url}&directory=${directory}`, form, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) + Promise.all( + Array.from(formData).map((f) => + getFileUploadUrl(uuid).then((url) => + axios.post(`${url}&directory=${directory}`, f, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (data: ProgressEvent) => { + // @ts-expect-error this is valid + const name = f.getAll('files')[0].name; + + appendFileUpload({ + name: name, + loaded: data.loaded, + total: data.total, + }); + + if (data.loaded === data.total) { + const timeout = setTimeout(() => removeFileUpload(name), 2000); + setTimeouts((t) => [...t, timeout]); + } + }, + }) + ) ) + ) .then(() => mutate()) .catch((error) => { console.error(error); clearAndAddHttpError({ error, key: 'files' }); - }) - .then(() => setVisible(false)) - .then(() => setLoading(false)); + }); }; return ( @@ -97,14 +141,13 @@ export default ({ className }: WithClassname) => { onFileSubmission(e.dataTransfer.files); }} > -
+

Drag and drop files to upload.

- { } }} /> - diff --git a/resources/scripts/state/server/files.ts b/resources/scripts/state/server/files.ts index f441b2919..7cdc7475d 100644 --- a/resources/scripts/state/server/files.ts +++ b/resources/scripts/state/server/files.ts @@ -1,19 +1,30 @@ import { action, Action } from 'easy-peasy'; import { cleanDirectoryPath } from '@/helpers'; +export interface FileUpload { + name: string; + loaded: number; + readonly total: number; +} + export interface ServerFileStore { directory: string; selectedFiles: string[]; + uploads: FileUpload[]; setDirectory: Action; setSelectedFiles: Action; appendSelectedFile: Action; removeSelectedFile: Action; + + appendFileUpload: Action; + removeFileUpload: Action; } const files: ServerFileStore = { directory: '/', selectedFiles: [], + uploads: [], setDirectory: action((state, payload) => { state.directory = cleanDirectoryPath(payload); @@ -30,6 +41,14 @@ const files: ServerFileStore = { removeSelectedFile: action((state, payload) => { state.selectedFiles = state.selectedFiles.filter((f) => f !== payload); }), + + appendFileUpload: action((state, payload) => { + state.uploads = state.uploads.filter((f) => f.name !== payload.name).concat(payload); + }), + + removeFileUpload: action((state, payload) => { + state.uploads = state.uploads.filter((f) => f.name !== payload); + }), }; export default files;