diff --git a/package.json b/package.json index 29681e4d6..ae732bc31 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "formik": "^1.5.7", "jquery": "^3.3.1", "lodash": "^4.17.11", + "path": "^0.12.7", "query-string": "^6.7.0", "react": "^16.8.6", "react-dom": "^16.8.6", @@ -41,6 +42,7 @@ "@types/events": "^3.0.0", "@types/feather-icons": "^4.7.0", "@types/lodash": "^4.14.119", + "@types/node": "^12.6.9", "@types/query-string": "^6.3.0", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.4", diff --git a/resources/scripts/api/server/files/renameFile.ts b/resources/scripts/api/server/files/renameFile.ts new file mode 100644 index 000000000..ba483d99c --- /dev/null +++ b/resources/scripts/api/server/files/renameFile.ts @@ -0,0 +1,19 @@ +import http from '@/api/http'; + +interface Data { + renameFrom: string; + renameTo: string; +} + +export default (uuid: string, { renameFrom, renameTo }: Data): Promise => { + return new Promise((resolve, reject) => { + http.put(`/api/client/servers/${uuid}/files/rename`, { + // eslint-disable-next-line @typescript-eslint/camelcase + rename_from: renameFrom, + // eslint-disable-next-line @typescript-eslint/camelcase + rename_to: renameTo, + }) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx index be1e62e20..743babd81 100644 --- a/resources/scripts/components/elements/Modal.tsx +++ b/resources/scripts/components/elements/Modal.tsx @@ -4,9 +4,12 @@ import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; import { CSSTransition } from 'react-transition-group'; import Spinner from '@/components/elements/Spinner'; -interface Props { +export interface RequiredModalProps { visible: boolean; onDismissed: () => void; +} + +type Props = RequiredModalProps & { dismissable?: boolean; closeOnEscape?: boolean; closeOnBackground?: boolean; diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index ece264c8c..acaa14673 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -7,14 +7,16 @@ import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt'; import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt'; import { faFileDownload } from '@fortawesome/free-solid-svg-icons/faFileDownload'; import { faCopy } from '@fortawesome/free-solid-svg-icons/faCopy'; -import { faArrowsAltH } from '@fortawesome/free-solid-svg-icons/faArrowsAltH'; -import { faBalanceScaleLeft } from '@fortawesome/free-solid-svg-icons/faBalanceScaleLeft'; import { faLevelUpAlt } from '@fortawesome/free-solid-svg-icons/faLevelUpAlt'; +import RenameFileModal from '@/components/server/files/RenameFileModal'; + +type ModalType = 'rename' | 'move'; export default ({ file }: { file: FileObject }) => { const menu = createRef(); const [ visible, setVisible ] = useState(false); - const [posX, setPosX] = useState(0); + const [ modal, setModal ] = useState(null); + const [ posX, setPosX ] = useState(0); const windowListener = (e: MouseEvent) => { if (e.button === 2 || !visible || !menu.current) { @@ -37,7 +39,7 @@ export default ({ file }: { file: FileObject }) => { if (visible && menu.current) { menu.current.setAttribute( - 'style', `margin-top: -0.35rem; left: ${Math.round(posX - menu.current.clientWidth)}px` + 'style', `margin-top: -0.35rem; left: ${Math.round(posX - menu.current.clientWidth)}px`, ); } }, [ visible ]); @@ -54,18 +56,28 @@ export default ({ file }: { file: FileObject }) => { e.preventDefault(); if (!visible) { setPosX(e.clientX); + } else if (visible) { + setModal(null); } setVisible(!visible); }} > + {visible && + + setModal(null)}/> + + }
-
+
setModal('rename')} + > Rename
diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 85bc6f337..de0d1d215 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -9,6 +9,7 @@ import { CSSTransition } from 'react-transition-group'; import { Link } from 'react-router-dom'; import Spinner from '@/components/elements/Spinner'; import FileObjectRow from '@/components/server/files/FileObjectRow'; +import { getDirectoryFromHash } from '@/helpers'; export default () => { const [ loading, setLoading ] = useState(true); @@ -16,12 +17,10 @@ export default () => { const server = ServerContext.useStoreState(state => state.server.data!); const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); - const currentDirectory = window.location.hash.replace(/^#(\/)+/, '/'); - const load = () => { setLoading(true); clearFlashes(); - loadDirectory(server.uuid, currentDirectory) + loadDirectory(server.uuid, getDirectoryFromHash()) .then(files => { setFiles(files); setLoading(false); @@ -37,7 +36,7 @@ export default () => { }); }; - const breadcrumbs = (): { name: string; path?: string }[] => currentDirectory.split('/') + const breadcrumbs = (): { name: string; path?: string }[] => getDirectoryFromHash().split('/') .filter(directory => !!directory) .map((directory, index, dirs) => { if (index === dirs.length - 1) { @@ -87,7 +86,7 @@ export default () => {
{ files.map(file => ( - + )) }
diff --git a/resources/scripts/components/server/files/RenameFileModal.tsx b/resources/scripts/components/server/files/RenameFileModal.tsx new file mode 100644 index 000000000..8e5575991 --- /dev/null +++ b/resources/scripts/components/server/files/RenameFileModal.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import Modal, { RequiredModalProps } from '@/components/elements/Modal'; +import { Form, Formik, FormikActions } from 'formik'; +import { FileObject } from '@/api/server/files/loadDirectory'; +import Field from '@/components/elements/Field'; +import { getDirectoryFromHash } from '@/helpers'; +import { join } from 'path'; +import renameFile from '@/api/server/files/renameFile'; +import { ServerContext } from '@/state/server'; + +interface FormikValues { + name: string; +} + +export default ({ file, ...props }: RequiredModalProps & { file: FileObject }) => { + const server = ServerContext.useStoreState(state => state.server.data!); + + const submit = (values: FormikValues, { setSubmitting }: FormikActions) => { + const renameFrom = join(getDirectoryFromHash(), file.name); + const renameTo = join(getDirectoryFromHash(), values.name); + + renameFile(server.uuid, { renameFrom, renameTo }) + .then(() => props.onDismissed()) + .catch(error => { + setSubmitting(false); + console.error(error); + }); + }; + + return ( + + {({ isSubmitting }) => ( + +
+ +
+ +
+ +
+ )} +
+ ); +}; diff --git a/resources/scripts/helpers.ts b/resources/scripts/helpers.ts index 9cec33a01..c55efe66b 100644 --- a/resources/scripts/helpers.ts +++ b/resources/scripts/helpers.ts @@ -4,3 +4,10 @@ export function bytesToHuman (bytes: number): string { // @ts-ignore return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${['Bytes', 'kB', 'MB', 'GB', 'TB'][i]}`; } + +/** + * Returns the current directory for the given window. + */ +export function getDirectoryFromHash (): string { + return window.location.hash.replace(/^#(\/)+/, '/'); +} diff --git a/yarn.lock b/yarn.lock index f8765e4f4..26d46f35d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -857,6 +857,10 @@ version "4.14.119" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39" +"@types/node@^12.6.9": + version "12.6.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.9.tgz#ffeee23afdc19ab16e979338e7b536fdebbbaeaf" + "@types/prop-types@*": version "15.7.1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" @@ -5571,6 +5575,13 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + dependencies: + process "^0.11.1" + util "^0.10.3" + pbkdf2@^3.0.3: version "3.0.16" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" @@ -6266,7 +6277,7 @@ process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" -process@^0.11.10: +process@^0.11.1, process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"