Apply new eslint rules; default to prettier for styling
This commit is contained in:
parent
f22cce8881
commit
dc84af9937
218 changed files with 3876 additions and 3564 deletions
|
@ -15,30 +15,36 @@ interface FormikValues {
|
|||
}
|
||||
|
||||
interface File {
|
||||
file: string,
|
||||
mode: string,
|
||||
file: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
type OwnProps = RequiredModalProps & { files: File[] };
|
||||
|
||||
const ChmodFileModal = ({ files, ...props }: OwnProps) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
const setSelectedFiles = ServerContext.useStoreActions((actions) => actions.files.setSelectedFiles);
|
||||
|
||||
const submit = ({ mode }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => {
|
||||
clearFlashes('files');
|
||||
|
||||
mutate(data => data.map(f => f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f), false);
|
||||
mutate(
|
||||
(data) =>
|
||||
data.map((f) =>
|
||||
f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
const data = files.map(f => ({ file: f.file, mode: mode }));
|
||||
const data = files.map((f) => ({ file: f.file, mode: mode }));
|
||||
|
||||
chmodFiles(uuid, directory, data)
|
||||
.then((): Promise<any> => files.length > 0 ? mutate() : Promise.resolve())
|
||||
.then((): Promise<any> => (files.length > 0 ? mutate() : Promise.resolve()))
|
||||
.then(() => setSelectedFiles([]))
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
mutate();
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ key: 'files', error });
|
||||
|
@ -47,19 +53,13 @@ const ChmodFileModal = ({ files, ...props }: OwnProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Formik onSubmit={submit} initialValues={{ mode: files.length > 1 ? '' : (files[0].mode || '') }}>
|
||||
<Formik onSubmit={submit} initialValues={{ mode: files.length > 1 ? '' : files[0].mode || '' }}>
|
||||
{({ isSubmitting }) => (
|
||||
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
|
||||
<Form css={tw`m-0`}>
|
||||
<div css={tw`flex flex-wrap items-end`}>
|
||||
<div css={tw`w-full sm:flex-1 sm:mr-4`}>
|
||||
<Field
|
||||
type={'string'}
|
||||
id={'file_mode'}
|
||||
name={'mode'}
|
||||
label={'File Mode'}
|
||||
autoFocus
|
||||
/>
|
||||
<Field type={'string'} id={'file_mode'} name={'mode'} label={'File Mode'} autoFocus />
|
||||
</div>
|
||||
<div css={tw`w-full sm:w-auto mt-4 sm:mt-0`}>
|
||||
<Button css={tw`w-full`}>Update</Button>
|
||||
|
|
|
@ -37,7 +37,8 @@ type ModalType = 'rename' | 'move' | 'chmod';
|
|||
|
||||
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`};
|
||||
${(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<HTMLDivElement> {
|
||||
|
@ -48,21 +49,21 @@ interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||
|
||||
const Row = ({ icon, title, ...props }: RowProps) => (
|
||||
<StyledRow {...props}>
|
||||
<FontAwesomeIcon icon={icon} css={tw`text-xs`} fixedWidth/>
|
||||
<FontAwesomeIcon icon={icon} css={tw`text-xs`} fixedWidth />
|
||||
<span css={tw`ml-2`}>{title}</span>
|
||||
</StyledRow>
|
||||
);
|
||||
|
||||
const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
||||
const onClickRef = useRef<DropdownMenu>(null);
|
||||
const [ showSpinner, setShowSpinner ] = useState(false);
|
||||
const [ modal, setModal ] = useState<ModalType | null>(null);
|
||||
const [ showConfirmation, setShowConfirmation ] = useState(false);
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
const [modal, setModal] = useState<ModalType | null>(null);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearAndAddHttpError, clearFlashes } = useFlash();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => {
|
||||
if (onClickRef.current) {
|
||||
|
@ -75,9 +76,9 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
|
||||
// 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.key !== file.key), false);
|
||||
mutate((files) => files.filter((f) => f.key !== file.key), false);
|
||||
|
||||
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
|
||||
deleteFiles(uuid, directory, [file.name]).catch((error) => {
|
||||
mutate();
|
||||
clearAndAddHttpError({ key: 'files', error });
|
||||
});
|
||||
|
@ -89,7 +90,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
|
||||
copyFile(uuid, join(directory, file.name))
|
||||
.then(() => mutate())
|
||||
.catch(error => clearAndAddHttpError({ key: 'files', error }))
|
||||
.catch((error) => clearAndAddHttpError({ key: 'files', error }))
|
||||
.then(() => setShowSpinner(false));
|
||||
};
|
||||
|
||||
|
@ -98,11 +99,11 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
clearFlashes('files');
|
||||
|
||||
getFileDownloadUrl(uuid, join(directory, file.name))
|
||||
.then(url => {
|
||||
.then((url) => {
|
||||
// @ts-ignore
|
||||
window.location = url;
|
||||
})
|
||||
.catch(error => clearAndAddHttpError({ key: 'files', error }))
|
||||
.catch((error) => clearAndAddHttpError({ key: 'files', error }))
|
||||
.then(() => setShowSpinner(false));
|
||||
};
|
||||
|
||||
|
@ -110,9 +111,9 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
setShowSpinner(true);
|
||||
clearFlashes('files');
|
||||
|
||||
compressFiles(uuid, directory, [ file.name ])
|
||||
compressFiles(uuid, directory, [file.name])
|
||||
.then(() => mutate())
|
||||
.catch(error => clearAndAddHttpError({ key: 'files', error }))
|
||||
.catch((error) => clearAndAddHttpError({ key: 'files', error }))
|
||||
.then(() => setShowSpinner(false));
|
||||
};
|
||||
|
||||
|
@ -122,7 +123,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
|
||||
decompressFiles(uuid, directory, file.name)
|
||||
.then(() => mutate())
|
||||
.catch(error => clearAndAddHttpError({ key: 'files', error }))
|
||||
.catch((error) => clearAndAddHttpError({ key: 'files', error }))
|
||||
.then(() => setShowSpinner(false));
|
||||
};
|
||||
|
||||
|
@ -140,55 +141,53 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
|||
</Dialog.Confirm>
|
||||
<DropdownMenu
|
||||
ref={onClickRef}
|
||||
renderToggle={onClick => (
|
||||
renderToggle={(onClick) => (
|
||||
<div css={tw`p-3 hover:text-white`} onClick={onClick}>
|
||||
<FontAwesomeIcon icon={faEllipsisH}/>
|
||||
{modal ?
|
||||
modal === 'chmod' ?
|
||||
<FontAwesomeIcon icon={faEllipsisH} />
|
||||
{modal ? (
|
||||
modal === 'chmod' ? (
|
||||
<ChmodFileModal
|
||||
visible
|
||||
appear
|
||||
files={[ { file: file.name, mode: file.modeBits } ]}
|
||||
files={[{ file: file.name, mode: file.modeBits }]}
|
||||
onDismissed={() => setModal(null)}
|
||||
/>
|
||||
:
|
||||
) : (
|
||||
<RenameFileModal
|
||||
visible
|
||||
appear
|
||||
files={[ file.name ]}
|
||||
files={[file.name]}
|
||||
useMoveTerminology={modal === 'move'}
|
||||
onDismissed={() => setModal(null)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
<SpinnerOverlay visible={showSpinner} fixed size={'large'}/>
|
||||
)
|
||||
) : null}
|
||||
<SpinnerOverlay visible={showSpinner} fixed size={'large'} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Can action={'file.update'}>
|
||||
<Row onClick={() => setModal('rename')} icon={faPencilAlt} title={'Rename'}/>
|
||||
<Row onClick={() => setModal('move')} icon={faLevelUpAlt} title={'Move'}/>
|
||||
<Row onClick={() => setModal('chmod')} icon={faFileCode} title={'Permissions'}/>
|
||||
<Row onClick={() => setModal('rename')} icon={faPencilAlt} title={'Rename'} />
|
||||
<Row onClick={() => setModal('move')} icon={faLevelUpAlt} title={'Move'} />
|
||||
<Row onClick={() => setModal('chmod')} icon={faFileCode} title={'Permissions'} />
|
||||
</Can>
|
||||
{file.isFile &&
|
||||
<Can action={'file.create'}>
|
||||
<Row onClick={doCopy} icon={faCopy} title={'Copy'}/>
|
||||
</Can>
|
||||
}
|
||||
{file.isArchiveType() ?
|
||||
{file.isFile && (
|
||||
<Can action={'file.create'}>
|
||||
<Row onClick={doUnarchive} icon={faBoxOpen} title={'Unarchive'}/>
|
||||
<Row onClick={doCopy} icon={faCopy} title={'Copy'} />
|
||||
</Can>
|
||||
:
|
||||
)}
|
||||
{file.isArchiveType() ? (
|
||||
<Can action={'file.create'}>
|
||||
<Row onClick={doUnarchive} icon={faBoxOpen} title={'Unarchive'} />
|
||||
</Can>
|
||||
) : (
|
||||
<Can action={'file.archive'}>
|
||||
<Row onClick={doArchive} icon={faFileArchive} title={'Archive'}/>
|
||||
<Row onClick={doArchive} icon={faFileArchive} title={'Archive'} />
|
||||
</Can>
|
||||
}
|
||||
{file.isFile &&
|
||||
<Row onClick={doDownload} icon={faFileDownload} title={'Download'}/>
|
||||
}
|
||||
)}
|
||||
{file.isFile && <Row onClick={doDownload} icon={faFileDownload} title={'Download'} />}
|
||||
<Can action={'file.delete'}>
|
||||
<Row onClick={() => setShowConfirmation(true)} icon={faTrashAlt} title={'Delete'} $danger/>
|
||||
<Row onClick={() => setShowConfirmation(true)} icon={faTrashAlt} title={'Delete'} $danger />
|
||||
</Can>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
|
|
|
@ -22,19 +22,19 @@ import { dirname } from 'path';
|
|||
import CodemirrorEditor from '@/components/elements/CodemirrorEditor';
|
||||
|
||||
export default () => {
|
||||
const [ error, setError ] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { action } = useParams<{ action: 'new' | string }>();
|
||||
const [ loading, setLoading ] = useState(action === 'edit');
|
||||
const [ content, setContent ] = useState('');
|
||||
const [ modalVisible, setModalVisible ] = useState(false);
|
||||
const [ mode, setMode ] = useState('text/plain');
|
||||
const [loading, setLoading] = useState(action === 'edit');
|
||||
const [content, setContent] = useState('');
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [mode, setMode] = useState('text/plain');
|
||||
|
||||
const history = useHistory();
|
||||
const { hash } = useLocation();
|
||||
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const id = ServerContext.useStoreState((state) => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const setDirectory = ServerContext.useStoreActions((actions) => actions.files.setDirectory);
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
|
||||
let fetchFileContent: null | (() => Promise<string>) = null;
|
||||
|
@ -48,12 +48,12 @@ export default () => {
|
|||
setDirectory(dirname(path));
|
||||
getFileContents(uuid, path)
|
||||
.then(setContent)
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setError(httpErrorToHuman(error));
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, [ action, uuid, hash ]);
|
||||
}, [action, uuid, hash]);
|
||||
|
||||
const save = (name?: string) => {
|
||||
if (!fetchFileContent) {
|
||||
|
@ -63,7 +63,7 @@ export default () => {
|
|||
setLoading(true);
|
||||
clearFlashes('files:view');
|
||||
fetchFileContent()
|
||||
.then(content => saveFileContents(uuid, name || hashToPath(hash), content))
|
||||
.then((content) => saveFileContents(uuid, name || hashToPath(hash), content))
|
||||
.then(() => {
|
||||
if (name) {
|
||||
history.push(`/server/${id}/files/edit#/${encodePathSegments(name)}`);
|
||||
|
@ -72,7 +72,7 @@ export default () => {
|
|||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
addError({ message: httpErrorToHuman(error), key: 'files:view' });
|
||||
})
|
||||
|
@ -80,31 +80,28 @@ export default () => {
|
|||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ServerError message={error} onBack={() => history.goBack()}/>
|
||||
);
|
||||
return <ServerError message={error} onBack={() => history.goBack()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
|
||||
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`} />
|
||||
<ErrorBoundary>
|
||||
<div css={tw`mb-4`}>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
|
||||
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
|
||||
<p css={tw`text-neutral-300 text-sm`}>
|
||||
You're editing
|
||||
a <code css={tw`font-mono bg-black rounded py-px px-1`}>.pteroignore</code> file.
|
||||
Any files or directories listed in here will be excluded from backups. Wildcards are supported by
|
||||
using an asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>). You can
|
||||
negate a prior rule by prepending an exclamation point
|
||||
(<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
{hash.replace(/^#/, '').endsWith('.pteroignore') && (
|
||||
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
|
||||
<p css={tw`text-neutral-300 text-sm`}>
|
||||
You're editing a <code css={tw`font-mono bg-black rounded py-px px-1`}>.pteroignore</code>{' '}
|
||||
file. Any files or directories listed in here will be excluded from backups. Wildcards are
|
||||
supported by using an asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>).
|
||||
You can negate a prior rule by prepending an exclamation point (
|
||||
<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<FileNameModal
|
||||
visible={modalVisible}
|
||||
onDismissed={() => setModalVisible(false)}
|
||||
|
@ -114,13 +111,13 @@ export default () => {
|
|||
}}
|
||||
/>
|
||||
<div css={tw`relative`}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
<SpinnerOverlay visible={loading} />
|
||||
<CodemirrorEditor
|
||||
mode={mode}
|
||||
filename={hash.replace(/^#/, '')}
|
||||
onModeChanged={setMode}
|
||||
initialContent={content}
|
||||
fetchContent={value => {
|
||||
fetchContent={(value) => {
|
||||
fetchFileContent = value;
|
||||
}}
|
||||
onContentSaved={() => {
|
||||
|
@ -134,27 +131,27 @@ export default () => {
|
|||
</div>
|
||||
<div css={tw`flex justify-end mt-4`}>
|
||||
<div css={tw`flex-1 sm:flex-none rounded bg-neutral-900 mr-4`}>
|
||||
<Select value={mode} onChange={e => setMode(e.currentTarget.value)}>
|
||||
{modes.map(mode => (
|
||||
<Select value={mode} onChange={(e) => setMode(e.currentTarget.value)}>
|
||||
{modes.map((mode) => (
|
||||
<option key={`${mode.name}_${mode.mime}`} value={mode.mime}>
|
||||
{mode.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{action === 'edit' ?
|
||||
{action === 'edit' ? (
|
||||
<Can action={'file.update'}>
|
||||
<Button css={tw`flex-1 sm:flex-none`} onClick={() => save()}>
|
||||
Save Content
|
||||
</Button>
|
||||
</Can>
|
||||
:
|
||||
) : (
|
||||
<Can action={'file.create'}>
|
||||
<Button css={tw`flex-1 sm:flex-none`} onClick={() => setModalVisible(true)}>
|
||||
Create File
|
||||
</Button>
|
||||
</Can>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</PageContentBlock>
|
||||
);
|
||||
|
|
|
@ -11,9 +11,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => {
|
||||
const [ file, setFile ] = useState<string | null>(null);
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const [file, setFile] = useState<string | null>(null);
|
||||
const id = ServerContext.useStoreState((state) => state.server.data!.id);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
const { hash } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -23,48 +23,49 @@ export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => {
|
|||
const name = path.split('/').pop() || null;
|
||||
setFile(name);
|
||||
}
|
||||
}, [ withinFileEditor, isNewFile, hash ]);
|
||||
}, [withinFileEditor, isNewFile, hash]);
|
||||
|
||||
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
||||
.filter(directory => !!directory)
|
||||
.map((directory, index, dirs) => {
|
||||
if (!withinFileEditor && index === dirs.length - 1) {
|
||||
return { name: directory };
|
||||
}
|
||||
const breadcrumbs = (): { name: string; path?: string }[] =>
|
||||
directory
|
||||
.split('/')
|
||||
.filter((directory) => !!directory)
|
||||
.map((directory, index, dirs) => {
|
||||
if (!withinFileEditor && index === dirs.length - 1) {
|
||||
return { name: directory };
|
||||
}
|
||||
|
||||
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||
});
|
||||
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||
});
|
||||
|
||||
return (
|
||||
<div css={tw`flex flex-grow-0 items-center text-sm text-neutral-500 overflow-x-hidden`}>
|
||||
{renderLeft || <div css={tw`w-12`}/>}
|
||||
/<span css={tw`px-1 text-neutral-300`}>home</span>/
|
||||
<NavLink
|
||||
to={`/server/${id}/files`}
|
||||
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
|
||||
>
|
||||
{renderLeft || <div css={tw`w-12`} />}/<span css={tw`px-1 text-neutral-300`}>home</span>/
|
||||
<NavLink to={`/server/${id}/files`} css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}>
|
||||
container
|
||||
</NavLink>/
|
||||
{
|
||||
breadcrumbs().map((crumb, index) => (
|
||||
crumb.path ?
|
||||
<React.Fragment key={index}>
|
||||
<NavLink
|
||||
to={`/server/${id}/files#${encodePathSegments(crumb.path)}`}
|
||||
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
|
||||
>
|
||||
{crumb.name}
|
||||
</NavLink>/
|
||||
</React.Fragment>
|
||||
:
|
||||
<span key={index} css={tw`px-1 text-neutral-300`}>{crumb.name}</span>
|
||||
))
|
||||
}
|
||||
{file &&
|
||||
<React.Fragment>
|
||||
<span css={tw`px-1 text-neutral-300`}>{file}</span>
|
||||
</React.Fragment>
|
||||
}
|
||||
</NavLink>
|
||||
/
|
||||
{breadcrumbs().map((crumb, index) =>
|
||||
crumb.path ? (
|
||||
<React.Fragment key={index}>
|
||||
<NavLink
|
||||
to={`/server/${id}/files#${encodePathSegments(crumb.path)}`}
|
||||
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
|
||||
>
|
||||
{crumb.name}
|
||||
</NavLink>
|
||||
/
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<span key={index} css={tw`px-1 text-neutral-300`}>
|
||||
{crumb.name}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{file && (
|
||||
<React.Fragment>
|
||||
<span css={tw`px-1 text-neutral-300`}>{file}</span>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,39 +23,39 @@ import { hashToPath } from '@/helpers';
|
|||
import style from './style.module.css';
|
||||
|
||||
const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||
const sortedFiles: FileObject[] = files.sort((a, b) => a.name.localeCompare(b.name)).sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1));
|
||||
const sortedFiles: FileObject[] = files
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => (a.isFile === b.isFile ? 0 : a.isFile ? 1 : -1));
|
||||
return sortedFiles.filter((file, index) => index === 0 || file.name !== sortedFiles[index - 1].name);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const id = ServerContext.useStoreState((state) => state.server.data!.id);
|
||||
const { hash } = useLocation();
|
||||
const { data: files, error, mutate } = useFileManagerSwr();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const clearFlashes = useStoreActions(actions => actions.flashes.clearFlashes);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
const clearFlashes = useStoreActions((actions) => actions.flashes.clearFlashes);
|
||||
const setDirectory = ServerContext.useStoreActions((actions) => actions.files.setDirectory);
|
||||
|
||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||
const selectedFilesLength = ServerContext.useStoreState(state => state.files.selectedFiles.length);
|
||||
const setSelectedFiles = ServerContext.useStoreActions((actions) => actions.files.setSelectedFiles);
|
||||
const selectedFilesLength = ServerContext.useStoreState((state) => state.files.selectedFiles.length);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('files');
|
||||
setSelectedFiles([]);
|
||||
setDirectory(hashToPath(hash));
|
||||
}, [ hash ]);
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate();
|
||||
}, [ directory ]);
|
||||
}, [directory]);
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedFiles(e.currentTarget.checked ? (files?.map(file => file.name) || []) : []);
|
||||
setSelectedFiles(e.currentTarget.checked ? files?.map((file) => file.name) || [] : []);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ServerError message={httpErrorToHuman(error)} onRetry={() => mutate()}/>
|
||||
);
|
||||
return <ServerError message={httpErrorToHuman(error)} onRetry={() => mutate()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -74,8 +74,8 @@ export default () => {
|
|||
/>
|
||||
<Can action={'file.create'}>
|
||||
<div className={style.manager_actions}>
|
||||
<NewDirectoryButton/>
|
||||
<UploadButton/>
|
||||
<NewDirectoryButton />
|
||||
<UploadButton />
|
||||
<NavLink to={`/server/${id}/files/new${window.location.hash}`}>
|
||||
<Button>New File</Button>
|
||||
</NavLink>
|
||||
|
@ -83,37 +83,32 @@ export default () => {
|
|||
</Can>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
{
|
||||
!files ?
|
||||
<Spinner size={'large'} centered/>
|
||||
:
|
||||
<>
|
||||
{!files.length ?
|
||||
<p css={tw`text-sm text-neutral-400 text-center`}>
|
||||
This directory seems to be empty.
|
||||
</p>
|
||||
:
|
||||
<CSSTransition classNames={'fade'} timeout={150} appear in>
|
||||
<div>
|
||||
{files.length > 250 &&
|
||||
<div css={tw`rounded bg-yellow-400 mb-px p-3`}>
|
||||
<p css={tw`text-yellow-900 text-sm text-center`}>
|
||||
This directory is too large to display in the browser,
|
||||
limiting the output to the first 250 files.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
sortFiles(files.slice(0, 250)).map(file => (
|
||||
<FileObjectRow key={file.key} file={file}/>
|
||||
))
|
||||
}
|
||||
<MassActionsBar/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{!files ? (
|
||||
<Spinner size={'large'} centered />
|
||||
) : (
|
||||
<>
|
||||
{!files.length ? (
|
||||
<p css={tw`text-sm text-neutral-400 text-center`}>This directory seems to be empty.</p>
|
||||
) : (
|
||||
<CSSTransition classNames={'fade'} timeout={150} appear in>
|
||||
<div>
|
||||
{files.length > 250 && (
|
||||
<div css={tw`rounded bg-yellow-400 mb-px p-3`}>
|
||||
<p css={tw`text-yellow-900 text-sm text-center`}>
|
||||
This directory is too large to display in the browser, limiting the output
|
||||
to the first 250 files.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{sortFiles(files.slice(0, 250)).map((file) => (
|
||||
<FileObjectRow key={file.key} file={file} />
|
||||
))}
|
||||
<MassActionsBar />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ interface Values {
|
|||
}
|
||||
|
||||
export default ({ onFileNamed, onDismissed, ...props }: Props) => {
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
onFileNamed(join(directory, values.fileName));
|
||||
|
|
|
@ -20,63 +20,53 @@ const Row = styled.div`
|
|||
`;
|
||||
|
||||
const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
|
||||
const [ canReadContents ] = usePermissions([ 'file.read-content' ]);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const [canReadContents] = usePermissions(['file.read-content']);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
(!canReadContents || (file.isFile && !file.isEditable())) ?
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default overflow-hidden truncate`}>
|
||||
{children}
|
||||
</div>
|
||||
:
|
||||
<NavLink
|
||||
to={`${match.url}${file.isFile ? '/edit' : ''}#${encodePathSegments(join(directory, file.name))}`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
return !canReadContents || (file.isFile && !file.isEditable()) ? (
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default overflow-hidden truncate`}>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<NavLink
|
||||
to={`${match.url}${file.isFile ? '/edit' : ''}#${encodePathSegments(join(directory, file.name))}`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
}, isEqual);
|
||||
|
||||
const FileObjectRow = ({ file }: { file: FileObject }) => (
|
||||
<Row
|
||||
key={file.name}
|
||||
onContextMenu={e => {
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.key}`, { detail: e.clientX }));
|
||||
}}
|
||||
>
|
||||
<SelectFileCheckbox name={file.name}/>
|
||||
<SelectFileCheckbox name={file.name} />
|
||||
<Clickable file={file}>
|
||||
<div css={tw`flex-none self-center text-neutral-400 ml-6 mr-4 text-lg pl-3`}>
|
||||
{file.isFile ?
|
||||
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}/>
|
||||
:
|
||||
<FontAwesomeIcon icon={faFolder}/>
|
||||
}
|
||||
{file.isFile ? (
|
||||
<FontAwesomeIcon
|
||||
icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
)}
|
||||
</div>
|
||||
<div css={tw`flex-1 truncate`}>
|
||||
{file.name}
|
||||
</div>
|
||||
{file.isFile &&
|
||||
<div css={tw`w-1/6 text-right mr-4 hidden sm:block`}>
|
||||
{bytesToString(file.size)}
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
css={tw`w-1/5 text-right mr-4 hidden md:block`}
|
||||
title={file.modifiedAt.toString()}
|
||||
>
|
||||
{Math.abs(differenceInHours(file.modifiedAt, new Date())) > 48 ?
|
||||
format(file.modifiedAt, 'MMM do, yyyy h:mma')
|
||||
:
|
||||
formatDistanceToNow(file.modifiedAt, { addSuffix: true })
|
||||
}
|
||||
<div css={tw`flex-1 truncate`}>{file.name}</div>
|
||||
{file.isFile && <div css={tw`w-1/6 text-right mr-4 hidden sm:block`}>{bytesToString(file.size)}</div>}
|
||||
<div css={tw`w-1/5 text-right mr-4 hidden md:block`} title={file.modifiedAt.toString()}>
|
||||
{Math.abs(differenceInHours(file.modifiedAt, new Date())) > 48
|
||||
? format(file.modifiedAt, 'MMM do, yyyy h:mma')
|
||||
: formatDistanceToNow(file.modifiedAt, { addSuffix: true })}
|
||||
</div>
|
||||
</Clickable>
|
||||
<FileDropdownMenu file={file}/>
|
||||
<FileDropdownMenu file={file} />
|
||||
</Row>
|
||||
);
|
||||
|
||||
|
|
|
@ -13,22 +13,22 @@ import Portal from '@/components/elements/Portal';
|
|||
import { Dialog } from '@/components/elements/dialog';
|
||||
|
||||
const MassActionsBar = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ loadingMessage, setLoadingMessage ] = useState('');
|
||||
const [ showConfirm, setShowConfirm ] = useState(false);
|
||||
const [ showMove, setShowMove ] = useState(false);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [showMove, setShowMove] = useState(false);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
const selectedFiles = ServerContext.useStoreState(state => state.files.selectedFiles);
|
||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||
const selectedFiles = ServerContext.useStoreState((state) => state.files.selectedFiles);
|
||||
const setSelectedFiles = ServerContext.useStoreActions((actions) => actions.files.setSelectedFiles);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) setLoadingMessage('');
|
||||
}, [ loading ]);
|
||||
}, [loading]);
|
||||
|
||||
const onClickCompress = () => {
|
||||
setLoading(true);
|
||||
|
@ -38,7 +38,7 @@ const MassActionsBar = () => {
|
|||
compressFiles(uuid, directory, selectedFiles)
|
||||
.then(() => mutate())
|
||||
.then(() => setSelectedFiles([]))
|
||||
.catch(error => clearAndAddHttpError({ key: 'files', error }))
|
||||
.catch((error) => clearAndAddHttpError({ key: 'files', error }))
|
||||
.then(() => setLoading(false));
|
||||
};
|
||||
|
||||
|
@ -50,10 +50,10 @@ const MassActionsBar = () => {
|
|||
|
||||
deleteFiles(uuid, directory, selectedFiles)
|
||||
.then(() => {
|
||||
mutate(files => files.filter(f => selectedFiles.indexOf(f.name) < 0), false);
|
||||
mutate((files) => files.filter((f) => selectedFiles.indexOf(f.name) < 0), false);
|
||||
setSelectedFiles([]);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
mutate();
|
||||
clearAndAddHttpError({ key: 'files', error });
|
||||
})
|
||||
|
@ -75,17 +75,15 @@ const MassActionsBar = () => {
|
|||
>
|
||||
<p className={'mb-2'}>
|
||||
Are you sure you want to delete
|
||||
<span className={'font-semibold text-gray-50'}>{selectedFiles.length} files</span>? This is
|
||||
a permanent action and the files cannot be recovered.
|
||||
<span className={'font-semibold text-gray-50'}>{selectedFiles.length} files</span>? This is a
|
||||
permanent action and the files cannot be recovered.
|
||||
</p>
|
||||
{selectedFiles.slice(0, 15).map(file => (
|
||||
<li key={file}>{file}</li>))
|
||||
}
|
||||
{selectedFiles.length > 15 &&
|
||||
<li>and {selectedFiles.length - 15} others</li>
|
||||
}
|
||||
{selectedFiles.slice(0, 15).map((file) => (
|
||||
<li key={file}>{file}</li>
|
||||
))}
|
||||
{selectedFiles.length > 15 && <li>and {selectedFiles.length - 15} others</li>}
|
||||
</Dialog.Confirm>
|
||||
{showMove &&
|
||||
{showMove && (
|
||||
<RenameFileModal
|
||||
files={selectedFiles}
|
||||
visible
|
||||
|
@ -93,7 +91,7 @@ const MassActionsBar = () => {
|
|||
useMoveTerminology
|
||||
onDismissed={() => setShowMove(false)}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<Portal>
|
||||
<div className={'fixed bottom-0 mb-6 flex justify-center w-full z-50'}>
|
||||
<Fade timeout={75} in={selectedFiles.length > 0} unmountOnExit>
|
||||
|
|
|
@ -40,12 +40,12 @@ const generateDirectoryData = (name: string): FileObject => ({
|
|||
});
|
||||
|
||||
export default ({ className }: WithClassname) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
@ -53,13 +53,13 @@ export default ({ className }: WithClassname) => {
|
|||
return () => {
|
||||
clearFlashes('files:directory-modal');
|
||||
};
|
||||
}, [ visible ]);
|
||||
}, [visible]);
|
||||
|
||||
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
createDirectory(uuid, directory, directoryName)
|
||||
.then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false))
|
||||
.then(() => mutate((data) => [...data, generateDirectoryData(directoryName)], false))
|
||||
.then(() => setVisible(false))
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ key: 'files:directory-modal', error });
|
||||
|
@ -69,11 +69,7 @@ export default ({ className }: WithClassname) => {
|
|||
return (
|
||||
<>
|
||||
<Portal>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
validationSchema={schema}
|
||||
initialValues={{ directoryName: '' }}
|
||||
>
|
||||
<Formik onSubmit={submit} validationSchema={schema} initialValues={{ directoryName: '' }}>
|
||||
{({ resetForm, submitForm, isSubmitting: _, values }) => (
|
||||
<Dialog
|
||||
title={'Create Directory'}
|
||||
|
@ -83,17 +79,13 @@ export default ({ className }: WithClassname) => {
|
|||
resetForm();
|
||||
}}
|
||||
>
|
||||
<FlashMessageRender key={'files:directory-modal'}/>
|
||||
<FlashMessageRender key={'files:directory-modal'} />
|
||||
<Form css={tw`m-0`}>
|
||||
<Field
|
||||
autoFocus
|
||||
id={'directoryName'}
|
||||
name={'directoryName'}
|
||||
label={'Name'}
|
||||
/>
|
||||
<Field autoFocus id={'directoryName'} name={'directoryName'} label={'Name'} />
|
||||
<p css={tw`mt-2 text-sm md:text-base break-all`}>
|
||||
<span css={tw`text-neutral-200`}>This directory will be created as </span>
|
||||
<Code>/home/container/
|
||||
<Code>
|
||||
/home/container/
|
||||
<span css={tw`text-cyan-200`}>
|
||||
{join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}
|
||||
</span>
|
||||
|
@ -110,7 +102,9 @@ export default ({ className }: WithClassname) => {
|
|||
>
|
||||
Cancel
|
||||
</Button.Text>
|
||||
<Button className={'w-full sm:w-auto'} onClick={submitForm}>Create</Button>
|
||||
<Button className={'w-full sm:w-auto'} onClick={submitForm}>
|
||||
Create
|
||||
</Button>
|
||||
</Dialog.Buttons>
|
||||
</Dialog>
|
||||
)}
|
||||
|
|
|
@ -17,11 +17,11 @@ interface FormikValues {
|
|||
type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
|
||||
|
||||
const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
const setSelectedFiles = ServerContext.useStoreActions((actions) => actions.files.setSelectedFiles);
|
||||
|
||||
const submit = ({ name }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => {
|
||||
clearFlashes('files');
|
||||
|
@ -30,24 +30,24 @@ const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
|||
if (files.length === 1) {
|
||||
if (!useMoveTerminology && len === 1) {
|
||||
// Rename the file within this directory.
|
||||
mutate(data => data.map(f => f.name === files[0] ? { ...f, name } : f), false);
|
||||
} else if ((useMoveTerminology || len > 1)) {
|
||||
mutate((data) => data.map((f) => (f.name === files[0] ? { ...f, name } : f)), false);
|
||||
} else if (useMoveTerminology || len > 1) {
|
||||
// Remove the file from this directory since they moved it elsewhere.
|
||||
mutate(data => data.filter(f => f.name !== files[0]), false);
|
||||
mutate((data) => data.filter((f) => f.name !== files[0]), false);
|
||||
}
|
||||
}
|
||||
|
||||
let data;
|
||||
if (useMoveTerminology && files.length > 1) {
|
||||
data = files.map(f => ({ from: f, to: join(name, f) }));
|
||||
data = files.map((f) => ({ from: f, to: join(name, f) }));
|
||||
} else {
|
||||
data = files.map(f => ({ from: f, to: name }));
|
||||
data = files.map((f) => ({ from: f, to: name }));
|
||||
}
|
||||
|
||||
renameFiles(uuid, directory, data)
|
||||
.then((): Promise<any> => files.length > 0 ? mutate() : Promise.resolve())
|
||||
.then((): Promise<any> => (files.length > 0 ? mutate() : Promise.resolve()))
|
||||
.then(() => setSelectedFiles([]))
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
mutate();
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ key: 'files', error });
|
||||
|
@ -56,25 +56,21 @@ const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Formik onSubmit={submit} initialValues={{ name: files.length > 1 ? '' : (files[0] || '') }}>
|
||||
<Formik onSubmit={submit} initialValues={{ name: files.length > 1 ? '' : files[0] || '' }}>
|
||||
{({ isSubmitting, values }) => (
|
||||
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
|
||||
<Form css={tw`m-0`}>
|
||||
<div
|
||||
css={[
|
||||
tw`flex flex-wrap`,
|
||||
useMoveTerminology ? tw`items-center` : tw`items-end`,
|
||||
]}
|
||||
>
|
||||
<div css={[tw`flex flex-wrap`, useMoveTerminology ? tw`items-center` : tw`items-end`]}>
|
||||
<div css={tw`w-full sm:flex-1 sm:mr-4`}>
|
||||
<Field
|
||||
type={'string'}
|
||||
id={'file_name'}
|
||||
name={'name'}
|
||||
label={'File Name'}
|
||||
description={useMoveTerminology
|
||||
? 'Enter the new name and directory of this file or folder, relative to the current directory.'
|
||||
: undefined
|
||||
description={
|
||||
useMoveTerminology
|
||||
? 'Enter the new name and directory of this file or folder, relative to the current directory.'
|
||||
: undefined
|
||||
}
|
||||
autoFocus
|
||||
/>
|
||||
|
@ -83,12 +79,12 @@ const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
|||
<Button css={tw`w-full`}>{useMoveTerminology ? 'Move' : 'Rename'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{useMoveTerminology &&
|
||||
<p css={tw`text-xs mt-2 text-neutral-400`}>
|
||||
<strong css={tw`text-neutral-200`}>New location:</strong>
|
||||
/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
|
||||
</p>
|
||||
}
|
||||
{useMoveTerminology && (
|
||||
<p css={tw`text-xs mt-2 text-neutral-400`}>
|
||||
<strong css={tw`text-neutral-200`}>New location:</strong>
|
||||
/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
|
||||
</p>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
|
|
|
@ -7,7 +7,7 @@ import Input from '@/components/elements/Input';
|
|||
export const FileActionCheckbox = styled(Input)`
|
||||
&& {
|
||||
${tw`border-neutral-500 bg-transparent`};
|
||||
|
||||
|
||||
&:not(:checked) {
|
||||
${tw`hover:border-neutral-300`};
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ export const FileActionCheckbox = styled(Input)`
|
|||
`;
|
||||
|
||||
export default ({ name }: { name: string }) => {
|
||||
const isChecked = ServerContext.useStoreState(state => state.files.selectedFiles.indexOf(name) >= 0);
|
||||
const appendSelectedFile = ServerContext.useStoreActions(actions => actions.files.appendSelectedFile);
|
||||
const removeSelectedFile = ServerContext.useStoreActions(actions => actions.files.removeSelectedFile);
|
||||
const isChecked = ServerContext.useStoreState((state) => state.files.selectedFiles.indexOf(name) >= 0);
|
||||
const appendSelectedFile = ServerContext.useStoreActions((actions) => actions.files.appendSelectedFile);
|
||||
const removeSelectedFile = ServerContext.useStoreActions((actions) => actions.files.removeSelectedFile);
|
||||
|
||||
return (
|
||||
<label css={tw`flex-none p-4 absolute self-center z-30 cursor-pointer`}>
|
||||
|
|
|
@ -15,28 +15,36 @@ import { WithClassname } from '@/components/types';
|
|||
import Portal from '@/components/elements/Portal';
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
max-width: 600px;
|
||||
${tw`bg-black w-full border-4 border-primary-500 border-dashed rounded p-10 mx-10`}
|
||||
max-width: 600px;
|
||||
${tw`bg-black w-full border-4 border-primary-500 border-dashed rounded p-10 mx-10`}
|
||||
`;
|
||||
|
||||
export default ({ className }: WithClassname) => {
|
||||
const fileUploadInput = useRef<HTMLInputElement>(null);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const directory = ServerContext.useStoreState((state) => state.files.directory);
|
||||
|
||||
useEventListener('dragenter', e => {
|
||||
e.stopPropagation();
|
||||
setVisible(true);
|
||||
}, true);
|
||||
useEventListener(
|
||||
'dragenter',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
setVisible(true);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useEventListener('dragexit', e => {
|
||||
e.stopPropagation();
|
||||
setVisible(false);
|
||||
}, true);
|
||||
useEventListener(
|
||||
'dragexit',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
setVisible(false);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
@ -47,22 +55,24 @@ export default ({ className }: WithClassname) => {
|
|||
return () => {
|
||||
window.removeEventListener('keydown', hide);
|
||||
};
|
||||
}, [ visible ]);
|
||||
}, [visible]);
|
||||
|
||||
const onFileSubmission = (files: FileList) => {
|
||||
const form = new FormData();
|
||||
Array.from(files).forEach(file => form.append('files', file));
|
||||
Array.from(files).forEach((file) => form.append('files', file));
|
||||
|
||||
setLoading(true);
|
||||
clearFlashes('files');
|
||||
getFileUploadUrl(uuid)
|
||||
.then(url => axios.post(`${url}&directory=${directory}`, form, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}))
|
||||
.then((url) =>
|
||||
axios.post(`${url}&directory=${directory}`, form, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(() => mutate())
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ error, key: 'files' });
|
||||
})
|
||||
|
@ -73,17 +83,11 @@ export default ({ className }: WithClassname) => {
|
|||
return (
|
||||
<>
|
||||
<Portal>
|
||||
<Fade
|
||||
appear
|
||||
in={visible}
|
||||
timeout={75}
|
||||
key={'upload_modal_mask'}
|
||||
unmountOnExit
|
||||
>
|
||||
<Fade appear in={visible} timeout={75} key={'upload_modal_mask'} unmountOnExit>
|
||||
<ModalMask
|
||||
onClick={() => setVisible(false)}
|
||||
onDragOver={e => e.preventDefault()}
|
||||
onDrop={e => {
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -95,20 +99,18 @@ export default ({ className }: WithClassname) => {
|
|||
>
|
||||
<div css={tw`w-full flex items-center justify-center`} style={{ pointerEvents: 'none' }}>
|
||||
<InnerContainer>
|
||||
<p css={tw`text-lg text-neutral-200 text-center`}>
|
||||
Drag and drop files to upload.
|
||||
</p>
|
||||
<p css={tw`text-lg text-neutral-200 text-center`}>Drag and drop files to upload.</p>
|
||||
</InnerContainer>
|
||||
</div>
|
||||
</ModalMask>
|
||||
</Fade>
|
||||
<SpinnerOverlay visible={loading} size={'large'} fixed/>
|
||||
<SpinnerOverlay visible={loading} size={'large'} fixed />
|
||||
</Portal>
|
||||
<input
|
||||
type={'file'}
|
||||
ref={fileUploadInput}
|
||||
css={tw`hidden`}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
if (!e.currentTarget.files) return;
|
||||
|
||||
onFileSubmission(e.currentTarget.files);
|
||||
|
@ -120,9 +122,7 @@ export default ({ className }: WithClassname) => {
|
|||
<Button
|
||||
className={className}
|
||||
onClick={() => {
|
||||
fileUploadInput.current
|
||||
? fileUploadInput.current.click()
|
||||
: setVisible(true);
|
||||
fileUploadInput.current ? fileUploadInput.current.click() : setVisible(true);
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
|
|
Reference in a new issue