Fix up file manager

This commit is contained in:
Dane Everitt 2020-07-04 17:57:24 -07:00
parent 7e8a5f1271
commit 43fbefbdb6
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
11 changed files with 108 additions and 104 deletions

View File

@ -1,9 +1,8 @@
import React, { useCallback, useEffect, useState, lazy } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import useRouter from 'use-react-router';
import { ServerContext } from '@/state/server';
import ace, { Editor } from 'brace'; import ace, { Editor } from 'brace';
import getFileContents from '@/api/server/files/getFileContents';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import tw from 'twin.macro';
import Select from '@/components/elements/Select';
// @ts-ignore // @ts-ignore
require('brace/ext/modelist'); require('brace/ext/modelist');
@ -11,7 +10,7 @@ require('ayu-ace/mirage');
const EditorContainer = styled.div` const EditorContainer = styled.div`
min-height: 16rem; min-height: 16rem;
height: calc(100vh - 16rem); height: calc(100vh - 20rem);
${tw`relative`}; ${tw`relative`};
#editor { #editor {
@ -20,9 +19,7 @@ const EditorContainer = styled.div`
`; `;
const modes: { [k: string]: string } = { const modes: { [k: string]: string } = {
// eslint-disable-next-line @typescript-eslint/camelcase
assembly_x86: 'Assembly (x86)', assembly_x86: 'Assembly (x86)',
// eslint-disable-next-line @typescript-eslint/camelcase
c_cpp: 'C++', c_cpp: 'C++',
coffee: 'Coffeescript', coffee: 'Coffeescript',
css: 'CSS', css: 'CSS',
@ -40,7 +37,6 @@ const modes: { [k: string]: string } = {
properties: 'Properties', properties: 'Properties',
python: 'Python', python: 'Python',
ruby: 'Ruby', ruby: 'Ruby',
// eslint-disable-next-line @typescript-eslint/camelcase
plain_text: 'Plaintext', plain_text: 'Plaintext',
toml: 'TOML', toml: 'TOML',
typescript: 'Typescript', typescript: 'Typescript',
@ -70,7 +66,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
useEffect(() => { useEffect(() => {
editor && editor.session.setMode(mode); editor && editor.session.setMode(mode);
}, [editor, mode]); }, [ editor, mode ]);
useEffect(() => { useEffect(() => {
editor && editor.session.setValue(initialContent || ''); editor && editor.session.setValue(initialContent || '');
@ -113,10 +109,9 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
return ( return (
<EditorContainer style={style}> <EditorContainer style={style}>
<div id={'editor'} ref={ref}/> <div id={'editor'} ref={ref}/>
<div className={'absolute right-0 bottom-0 z-50'}> <div css={tw`absolute right-0 bottom-0 z-50`}>
<div className={'m-3 rounded bg-neutral-900 border border-black'}> <div css={tw`m-3 rounded bg-neutral-900 border border-black`}>
<select <Select
className={'input-dark'}
value={mode.split('/').pop()} value={mode.split('/').pop()}
onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)} onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)}
> >
@ -125,7 +120,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
<option key={key} value={key}>{modes[key]}</option> <option key={key} value={key}>{modes[key]}</option>
)) ))
} }
</select> </Select>
</div> </div>
</div> </div>
</EditorContainer> </EditorContainer>

View File

@ -69,8 +69,7 @@ const DropdownMenu = ({ renderToggle, children }: Props) => {
e.stopPropagation(); e.stopPropagation();
setVisible(false); setVisible(false);
}} }}
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500`} css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
style={{ minWidth: '12rem' }}
> >
{children} {children}
</div> </div>

View File

@ -18,6 +18,8 @@ import Can from '@/components/elements/Can';
import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl'; import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl';
import useServer from '@/plugins/useServer'; import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import tw from 'twin.macro';
import Fade from '@/components/elements/Fade';
type ModalType = 'rename' | 'move'; type ModalType = 'rename' | 'move';
@ -113,7 +115,7 @@ export default ({ uuid }: { uuid: string }) => {
<div key={`dropdown:${file.uuid}`}> <div key={`dropdown:${file.uuid}`}>
<div <div
ref={menuButton} ref={menuButton}
className={'p-3 hover:text-white'} css={tw`p-3 hover:text-white`}
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (!menuVisible) { if (!menuVisible) {
@ -133,60 +135,60 @@ export default ({ uuid }: { uuid: string }) => {
setMenuVisible(false); setMenuVisible(false);
}} }}
/> />
<SpinnerOverlay visible={showSpinner} fixed={true} size={'large'}/> <SpinnerOverlay visible={showSpinner} fixed size={'large'}/>
</div> </div>
<CSSTransition timeout={250} in={menuVisible} unmountOnExit={true} classNames={'fade'}> <Fade timeout={250} in={menuVisible} unmountOnExit classNames={'fade'}>
<div <div
ref={menu} ref={menu}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
setMenuVisible(false); setMenuVisible(false);
}} }}
className={'absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48'} css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
> >
<Can action={'file.update'}> <Can action={'file.update'}>
<div <div
onClick={() => setModal('rename')} onClick={() => setModal('rename')}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'} css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
> >
<FontAwesomeIcon icon={faPencilAlt} className={'text-xs'}/> <FontAwesomeIcon icon={faPencilAlt} css={tw`text-xs`}/>
<span className={'ml-2'}>Rename</span> <span css={tw`ml-2`}>Rename</span>
</div> </div>
<div <div
onClick={() => setModal('move')} onClick={() => setModal('move')}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'} css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
> >
<FontAwesomeIcon icon={faLevelUpAlt} className={'text-xs'}/> <FontAwesomeIcon icon={faLevelUpAlt} css={tw`text-xs`}/>
<span className={'ml-2'}>Move</span> <span css={tw`ml-2`}>Move</span>
</div> </div>
</Can> </Can>
<Can action={'file.create'}> <Can action={'file.create'}>
<div <div
onClick={() => doCopy()} onClick={() => doCopy()}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'} css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
> >
<FontAwesomeIcon icon={faCopy} className={'text-xs'}/> <FontAwesomeIcon icon={faCopy} css={tw`text-xs`}/>
<span className={'ml-2'}>Copy</span> <span css={tw`ml-2`}>Copy</span>
</div> </div>
</Can> </Can>
<div <div
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'} css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
onClick={() => doDownload()} onClick={() => doDownload()}
> >
<FontAwesomeIcon icon={faFileDownload} className={'text-xs'}/> <FontAwesomeIcon icon={faFileDownload} css={tw`text-xs`}/>
<span className={'ml-2'}>Download</span> <span css={tw`ml-2`}>Download</span>
</div> </div>
<Can action={'file.delete'}> <Can action={'file.delete'}>
<div <div
onClick={() => doDeletion()} onClick={() => doDeletion()}
className={'hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded'} css={tw`hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded`}
> >
<FontAwesomeIcon icon={faTrashAlt} className={'text-xs'}/> <FontAwesomeIcon icon={faTrashAlt} css={tw`text-xs`}/>
<span className={'ml-2'}>Delete</span> <span css={tw`ml-2`}>Delete</span>
</div> </div>
</Can> </Can>
</div> </div>
</CSSTransition> </Fade>
</div> </div>
); );
}; };

View File

@ -14,6 +14,8 @@ import Can from '@/components/elements/Can';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError'; import ServerError from '@/components/screens/ServerError';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor')); const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor'));
@ -81,16 +83,17 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'files:view'} className={'mb-4'}/> <FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<FileManagerBreadcrumbs withinFileEditor={true} isNewFile={action !== 'edit'}/> <FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
{(name || hash.replace(/^#/, '')).endsWith('.pteroignore') && {hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div className={'mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400'}> <div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p className={'text-neutral-300 text-sm'}> <p css={tw`text-neutral-300 text-sm`}>
You're editing a <code className={'font-mono bg-black rounded py-px px-1'}>.pteroignore</code> file. You&apos;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 Any files or directories listed in here will be excluded from backups. Wildcards are supported by
using an asterisk (<code className={'font-mono bg-black rounded py-px px-1'}>*</code>). You can 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 negate a prior rule by prepending an exclamation point
(<code className={'font-mono bg-black rounded py-px px-1'}>!</code>). (<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
</p> </p>
</div> </div>
} }
@ -102,7 +105,7 @@ export default () => {
save(name); save(name);
}} }}
/> />
<div className={'relative'}> <div css={tw`relative`}>
<SpinnerOverlay visible={loading}/> <SpinnerOverlay visible={loading}/>
<LazyAceEditor <LazyAceEditor
initialModePath={hash.replace(/^#/, '') || 'plain_text'} initialModePath={hash.replace(/^#/, '') || 'plain_text'}
@ -113,18 +116,18 @@ export default () => {
onContentSaved={() => save()} onContentSaved={() => save()}
/> />
</div> </div>
<div className={'flex justify-end mt-4'}> <div css={tw`flex justify-end mt-4`}>
{action === 'edit' ? {action === 'edit' ?
<Can action={'file.update'}> <Can action={'file.update'}>
<button className={'btn btn-primary btn-sm'} onClick={() => save()}> <Button onClick={() => save()}>
Save Content Save Content
</button> </Button>
</Can> </Can>
: :
<Can action={'file.create'}> <Can action={'file.create'}>
<button className={'btn btn-primary btn-sm'} onClick={() => setModalVisible(true)}> <Button onClick={() => setModalVisible(true)}>
Create File Create File
</button> </Button>
</Can> </Can>
} }
</div> </div>

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { cleanDirectoryPath } from '@/helpers'; import { cleanDirectoryPath } from '@/helpers';
import tw from 'twin.macro';
interface Props { interface Props {
withinFileEditor?: boolean; withinFileEditor?: boolean;
@ -32,11 +33,11 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
}); });
return ( return (
<div className={'flex items-center text-sm mb-4 text-neutral-500'}> <div css={tw`flex items-center text-sm mb-4 text-neutral-500`}>
/<span className={'px-1 text-neutral-300'}>home</span>/ /<span css={tw`px-1 text-neutral-300`}>home</span>/
<NavLink <NavLink
to={`/server/${id}/files`} to={`/server/${id}/files`}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'} css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
> >
container container
</NavLink>/ </NavLink>/
@ -46,18 +47,18 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
<React.Fragment key={index}> <React.Fragment key={index}>
<NavLink <NavLink
to={`/server/${id}/files#${crumb.path}`} to={`/server/${id}/files#${crumb.path}`}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'} css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
> >
{crumb.name} {crumb.name}
</NavLink>/ </NavLink>/
</React.Fragment> </React.Fragment>
: :
<span key={index} className={'px-1 text-neutral-300'}>{crumb.name}</span> <span key={index} css={tw`px-1 text-neutral-300`}>{crumb.name}</span>
)) ))
} }
{file && {file &&
<React.Fragment> <React.Fragment>
<span className={'px-1 text-neutral-300'}>{decodeURIComponent(file)}</span> <span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span>
</React.Fragment> </React.Fragment>
} }
</div> </div>

View File

@ -14,7 +14,8 @@ import { Link } from 'react-router-dom';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError'; import ServerError from '@/components/screens/ServerError';
import useRouter from 'use-react-router'; import tw from 'twin.macro';
import Button from '@/components/elements/Button';
const sortFiles = (files: FileObject[]): FileObject[] => { const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name)) return files.sort((a, b) => a.name.localeCompare(b.name))
@ -24,7 +25,7 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
export default () => { export default () => {
const [ error, setError ] = useState(''); const [ error, setError ] = useState('');
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { id } = ServerContext.useStoreState(state => state.server.data!); const { id } = ServerContext.useStoreState(state => state.server.data!);
const { contents: files } = ServerContext.useStoreState(state => state.files); const { contents: files } = ServerContext.useStoreState(state => state.files);
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files); const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
@ -56,16 +57,16 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'files'} className={'mb-4'}/> <FlashMessageRender byKey={'files'} css={tw`mb-4`}/>
<React.Fragment> <React.Fragment>
<FileManagerBreadcrumbs/> <FileManagerBreadcrumbs/>
{ {
loading ? loading ?
<Spinner size={'large'} centered={true}/> <Spinner size={'large'} centered/>
: :
<React.Fragment> <React.Fragment>
{!files.length ? {!files.length ?
<p className={'text-sm text-neutral-400 text-center'}> <p css={tw`text-sm text-neutral-400 text-center`}>
This directory seems to be empty. This directory seems to be empty.
</p> </p>
: :
@ -74,8 +75,8 @@ export default () => {
<div> <div>
{files.length > 250 ? {files.length > 250 ?
<React.Fragment> <React.Fragment>
<div className={'rounded bg-yellow-400 mb-px p-3'}> <div css={tw`rounded bg-yellow-400 mb-px p-3`}>
<p className={'text-yellow-900 text-sm text-center'}> <p css={tw`text-yellow-900 text-sm text-center`}>
This directory is too large to display in the browser, This directory is too large to display in the browser,
limiting the output to the first 250 files. limiting the output to the first 250 files.
</p> </p>
@ -96,14 +97,15 @@ export default () => {
</CSSTransition> </CSSTransition>
} }
<Can action={'file.create'}> <Can action={'file.create'}>
<div className={'flex justify-end mt-8'}> <div css={tw`flex justify-end mt-8`}>
<NewDirectoryButton/> <NewDirectoryButton/>
<Link <Button
// @ts-ignore
as={Link}
to={`/server/${id}/files/new${window.location.hash}`} to={`/server/${id}/files/new${window.location.hash}`}
className={'btn btn-sm btn-primary'}
> >
New File New File
</Link> </Button>
</div> </div>
</Can> </Can>
</React.Fragment> </React.Fragment>

View File

@ -5,6 +5,8 @@ import { object, string } from 'yup';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { join } from 'path'; import { join } from 'path';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
type Props = RequiredModalProps & { type Props = RequiredModalProps & {
onFileNamed: (name: string) => void; onFileNamed: (name: string) => void;
@ -44,12 +46,10 @@ export default ({ onFileNamed, onDismissed, ...props }: Props) => {
name={'fileName'} name={'fileName'}
label={'File Name'} label={'File Name'}
description={'Enter the name that this file should be saved as.'} description={'Enter the name that this file should be saved as.'}
autoFocus={true} autoFocus
/> />
<div className={'mt-6 text-right'}> <div css={tw`mt-6 text-right`}>
<button className={'btn btn-primary btn-sm'}> <Button>Create File</Button>
Create File
</button>
</div> </div>
</Form> </Form>
</Modal> </Modal>

View File

@ -10,6 +10,7 @@ import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import useRouter from 'use-react-router'; import useRouter from 'use-react-router';
import tw from 'twin.macro';
export default ({ file }: { file: FileObject }) => { export default ({ file }: { file: FileObject }) => {
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
@ -19,14 +20,11 @@ export default ({ file }: { file: FileObject }) => {
return ( return (
<div <div
key={file.name} key={file.name}
className={` css={tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`}
flex bg-neutral-700 rounded-sm mb-px text-sm
hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600
`}
> >
<NavLink <NavLink
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`} to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
className={'flex flex-1 text-neutral-300 no-underline p-3'} css={tw`flex flex-1 text-neutral-300 no-underline p-3`}
onClick={e => { onClick={e => {
// Don't rely on the onClick to work with the generated URL. Because of the way this // Don't rely on the onClick to work with the generated URL. Because of the way this
// component re-renders you'll get redirected into a nested directory structure since // component re-renders you'll get redirected into a nested directory structure since
@ -41,27 +39,27 @@ export default ({ file }: { file: FileObject }) => {
} }
}} }}
> >
<div className={'flex-none text-neutral-400 mr-4 text-lg pl-3'}> <div css={tw`flex-none text-neutral-400 mr-4 text-lg pl-3`}>
{file.isFile ? {file.isFile ?
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : faFileAlt}/> <FontAwesomeIcon icon={file.isSymlink ? faFileImport : faFileAlt}/>
: :
<FontAwesomeIcon icon={faFolder}/> <FontAwesomeIcon icon={faFolder}/>
} }
</div> </div>
<div className={'flex-1'}> <div css={tw`flex-1`}>
{file.name} {file.name}
</div> </div>
{file.isFile && {file.isFile &&
<div className={'w-1/6 text-right mr-4'}> <div css={tw`w-1/6 text-right mr-4`}>
{bytesToHuman(file.size)} {bytesToHuman(file.size)}
</div> </div>
} }
<div <div
className={'w-1/5 text-right mr-4'} css={tw`w-1/5 text-right mr-4`}
title={file.modifiedAt.toString()} title={file.modifiedAt.toString()}
> >
{Math.abs(differenceInHours(file.modifiedAt, new Date())) > 48 ? {Math.abs(differenceInHours(file.modifiedAt, new Date())) > 48 ?
format(file.modifiedAt, 'MMM Do, YYYY h:mma') format(file.modifiedAt, 'MMM do, yyyy h:mma')
: :
formatDistanceToNow(file.modifiedAt, { addSuffix: true }) formatDistanceToNow(file.modifiedAt, { addSuffix: true })
} }

View File

@ -7,6 +7,8 @@ import { join } from 'path';
import { object, string } from 'yup'; import { object, string } from 'yup';
import createDirectory from '@/api/server/files/createDirectory'; import createDirectory from '@/api/server/files/createDirectory';
import v4 from 'uuid/v4'; import v4 from 'uuid/v4';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface Values { interface Values {
directoryName: string; directoryName: string;
@ -62,33 +64,33 @@ export default () => {
resetForm(); resetForm();
}} }}
> >
<Form className={'m-0'}> <Form css={tw`m-0`}>
<Field <Field
id={'directoryName'} id={'directoryName'}
name={'directoryName'} name={'directoryName'}
label={'Directory Name'} label={'Directory Name'}
/> />
<p className={'text-xs mt-2 text-neutral-400'}> <p css={tw`text-xs mt-2 text-neutral-400`}>
<span className={'text-neutral-200'}>This directory will be created as</span> <span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/ &nbsp;/home/container/
<span className={'text-cyan-200'}> <span css={tw`text-cyan-200`}>
{decodeURIComponent( {decodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''), join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)} )}
</span> </span>
</p> </p>
<div className={'flex justify-end'}> <div css={tw`flex justify-end`}>
<button className={'btn btn-sm btn-primary mt-8'}> <Button css={tw`mt-8`}>
Create Directory Create Directory
</button> </Button>
</div> </div>
</Form> </Form>
</Modal> </Modal>
)} )}
</Formik> </Formik>
<button className={'btn btn-sm btn-secondary mr-2'} onClick={() => setVisible(true)}> <Button isSecondary css={tw`mr-2`} onClick={() => setVisible(true)}>
Create Directory Create Directory
</button> </Button>
</React.Fragment> </React.Fragment>
); );
}; };

View File

@ -6,7 +6,8 @@ import { join } from 'path';
import renameFile from '@/api/server/files/renameFile'; import renameFile from '@/api/server/files/renameFile';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { FileObject } from '@/api/server/files/loadDirectory'; import { FileObject } from '@/api/server/files/loadDirectory';
import classNames from 'classnames'; import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface FormikValues { interface FormikValues {
name: string; name: string;
@ -48,14 +49,14 @@ export default ({ file, useMoveTerminology, ...props }: Props) => {
> >
{({ isSubmitting, values }) => ( {({ isSubmitting, values }) => (
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}> <Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
<Form className={'m-0'}> <Form css={tw`m-0`}>
<div <div
className={classNames('flex', { css={[
'items-center': useMoveTerminology, tw`flex`,
'items-end': !useMoveTerminology, useMoveTerminology ? tw`items-center` : tw`items-end`,
})} ]}
> >
<div className={'flex-1 mr-6'}> <div css={tw`flex-1 mr-6`}>
<Field <Field
type={'string'} type={'string'}
id={'file_name'} id={'file_name'}
@ -65,18 +66,16 @@ export default ({ file, useMoveTerminology, ...props }: Props) => {
? 'Enter the new name and directory of this file or folder, relative to the current directory.' ? 'Enter the new name and directory of this file or folder, relative to the current directory.'
: undefined : undefined
} }
autoFocus={true} autoFocus
/> />
</div> </div>
<div> <div>
<button className={'btn btn-sm btn-primary'}> <Button>{useMoveTerminology ? 'Move' : 'Rename'}</Button>
{useMoveTerminology ? 'Move' : 'Rename'}
</button>
</div> </div>
</div> </div>
{useMoveTerminology && {useMoveTerminology &&
<p className={'text-xs mt-2 text-neutral-400'}> <p css={tw`text-xs mt-2 text-neutral-400`}>
<strong className={'text-neutral-200'}>New location:</strong> <strong css={tw`text-neutral-200`}>New location:</strong>
&nbsp;/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')} &nbsp;/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
</p> </p>
} }

View File

@ -119,6 +119,9 @@ module.exports = {
transitionDuration: { transitionDuration: {
250: '250ms', 250: '250ms',
}, },
minWidth: {
'48': '12rem',
},
borderColor: theme => ({ borderColor: theme => ({
default: theme('colors.neutral.400', 'cuurrentColor'), default: theme('colors.neutral.400', 'cuurrentColor'),
}), }),