Fix up file manager
This commit is contained in:
parent
7e8a5f1271
commit
43fbefbdb6
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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'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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
/home/container/
|
/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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
|
/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in New Issue