diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index a1953470..69f98a89 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -1,4 +1,4 @@ -import React, { createRef, useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy, @@ -7,6 +7,7 @@ import { faLevelUpAlt, faPencilAlt, faTrashAlt, + IconDefinition, } from '@fortawesome/free-solid-svg-icons'; import RenameFileModal from '@/components/server/files/RenameFileModal'; import { ServerContext } from '@/state/server'; @@ -14,182 +15,110 @@ import { join } from 'path'; import deleteFile from '@/api/server/files/deleteFile'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import copyFile from '@/api/server/files/copyFile'; -import { httpErrorToHuman } from '@/api/http'; import Can from '@/components/elements/Can'; import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl'; import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; -import Fade from '@/components/elements/Fade'; +import { FileObject } from '@/api/server/files/loadDirectory'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; +import DropdownMenu from '@/components/elements/DropdownMenu'; +import styled from 'styled-components/macro'; type ModalType = 'rename' | 'move'; -export default ({ uuid }: { uuid: string }) => { - const menu = createRef(); - const menuButton = createRef(); - const [ menuVisible, setMenuVisible ] = useState(false); +const StyledRow = styled.div<{ $danger?: boolean }>` + ${tw`p-2 flex items-center rounded`}; + ${props => props.$danger ? tw`hover:bg-red-100 hover:text-red-700` : tw`hover:bg-neutral-100 hover:text-neutral-700`}; +`; + +interface RowProps extends React.HTMLAttributes { + icon: IconDefinition; + title: string; + $danger?: boolean; +} + +const Row = ({ icon, title, ...props }: RowProps) => ( + + + {title} + +); + +export default ({ file }: { file: FileObject }) => { const [ showSpinner, setShowSpinner ] = useState(false); const [ modal, setModal ] = useState(null); - const [ posX, setPosX ] = useState(0); - const server = useServer(); - const { addError, clearFlashes } = useFlash(); + const { uuid } = useServer(); + const { mutate } = useFileManagerSwr(); + const { clearAndAddHttpError, clearFlashes } = useFlash(); - const file = ServerContext.useStoreState(state => state.files.contents.find(file => file.uuid === uuid)); const directory = ServerContext.useStoreState(state => state.files.directory); - const { removeFile, getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files); - - if (!file) { - return null; - } - - const windowListener = (e: MouseEvent) => { - if (e.button === 2 || !menuVisible || !menu.current) { - return; - } - - if (e.target === menu.current || menu.current.contains(e.target as Node)) { - return; - } - - if (e.target !== menu.current && !menu.current.contains(e.target as Node)) { - setMenuVisible(false); - } - }; const doDeletion = () => { - setShowSpinner(true); clearFlashes('files'); - deleteFile(server.uuid, join(directory, file.name)) - .then(() => removeFile(uuid)) - .catch(error => { - console.error('Error while attempting to delete a file.', error); - addError({ key: 'files', message: httpErrorToHuman(error) }); - setShowSpinner(false); - }); + + // For UI speed, immediately remove the file from the listing before calling the deletion function. + // If the delete actually fails, we'll fetch the current directory contents again automatically. + mutate(files => files.filter(f => f.uuid !== file.uuid), false); + + deleteFile(uuid, join(directory, file.name)).catch(error => { + mutate(); + clearAndAddHttpError({ key: 'files', error }); + }); }; const doCopy = () => { setShowSpinner(true); clearFlashes('files'); - copyFile(server.uuid, join(directory, file.name)) - .then(() => getDirectoryContents(directory)) + + copyFile(uuid, join(directory, file.name)) + .then(() => mutate()) .catch(error => { - console.error('Error while attempting to copy file.', error); - addError({ key: 'files', message: httpErrorToHuman(error) }); setShowSpinner(false); + clearAndAddHttpError({ key: 'files', error }); }); }; const doDownload = () => { setShowSpinner(true); clearFlashes('files'); - getFileDownloadUrl(server.uuid, join(directory, file.name)) + + getFileDownloadUrl(uuid, join(directory, file.name)) .then(url => { // @ts-ignore window.location = url; }) - .catch(error => { - console.error(error); - addError({ key: 'files', message: httpErrorToHuman(error) }); - }) + .catch(error => clearAndAddHttpError({ key: 'files', error })) .then(() => setShowSpinner(false)); }; - useEffect(() => { - menuVisible - ? document.addEventListener('click', windowListener) - : document.removeEventListener('click', windowListener); - - if (menuVisible && menu.current) { - menu.current.setAttribute( - 'style', `margin-top: -0.35rem; left: ${Math.round(posX - menu.current.clientWidth)}px`, - ); - } - }, [ menuVisible ]); - - useEffect(() => () => { - document.removeEventListener('click', windowListener); - }, []); - return ( -
-
{ - e.preventDefault(); - if (!menuVisible) { - setPosX(e.clientX); - } - setModal(null); - setMenuVisible(!menuVisible); - }} - > - - { - setModal(null); - setMenuVisible(false); - }} - /> - -
- -
{ - e.stopPropagation(); - setMenuVisible(false); - }} - css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`} - > - -
setModal('rename')} - css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`} - > - - Rename -
-
setModal('move')} - css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`} - > - - Move -
-
- -
doCopy()} - css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`} - > - - Copy -
-
-
doDownload()} - > - - Download -
- -
doDeletion()} - css={tw`hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded`} - > - - Delete -
-
+ ( +
+ + setModal(null)} + /> +
- -
+ )} + > + + setModal('rename')} icon={faPencilAlt} title={'Rename'}/> + setModal('move')} icon={faLevelUpAlt} title={'Move'}/> + + + + + + + + + ); }; diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 35fc034e..1a1d1c75 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -4,7 +4,7 @@ import { CSSTransition } from 'react-transition-group'; import Spinner from '@/components/elements/Spinner'; import FileObjectRow from '@/components/server/files/FileObjectRow'; import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs'; -import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory'; +import { FileObject } from '@/api/server/files/loadDirectory'; import NewDirectoryButton from '@/components/server/files/NewDirectoryButton'; import { Link, useLocation } from 'react-router-dom'; import Can from '@/components/elements/Can'; @@ -12,10 +12,9 @@ import PageContentBlock from '@/components/elements/PageContentBlock'; import ServerError from '@/components/screens/ServerError'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import useSWR from 'swr'; import useServer from '@/plugins/useServer'; -import { cleanDirectoryPath } from '@/helpers'; import { ServerContext } from '@/state/server'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; const sortFiles = (files: FileObject[]): FileObject[] => { return files.sort((a, b) => a.name.localeCompare(b.name)) @@ -23,15 +22,11 @@ const sortFiles = (files: FileObject[]): FileObject[] => { }; export default () => { + const { id } = useServer(); const { hash } = useLocation(); - const { id, uuid } = useServer(); + const { data: files, error, mutate } = useFileManagerSwr(); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); - const { data: files, error, mutate } = useSWR( - `${uuid}:files:${hash}`, - () => loadDirectory(uuid, cleanDirectoryPath(window.location.hash)), - ); - useEffect(() => { setDirectory(hash.length > 0 ? hash : '/'); }, [ hash ]); diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index e6f15aed..66ed8ca0 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -69,7 +69,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => { }
- + ); }; diff --git a/resources/scripts/plugins/useFileManagerSwr.ts b/resources/scripts/plugins/useFileManagerSwr.ts new file mode 100644 index 00000000..c127aaee --- /dev/null +++ b/resources/scripts/plugins/useFileManagerSwr.ts @@ -0,0 +1,15 @@ +import useSWR from 'swr'; +import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory'; +import { cleanDirectoryPath } from '@/helpers'; +import useServer from '@/plugins/useServer'; +import { useLocation } from 'react-router'; + +export default () => { + const { uuid } = useServer(); + const { hash } = useLocation(); + + return useSWR( + `${uuid}:files:${hash}`, + () => loadDirectory(uuid, cleanDirectoryPath(hash)), + ); +};