Square away saving of existing files

This commit is contained in:
Dane Everitt 2019-10-26 13:16:27 -07:00
parent 0dff732883
commit 78ccdf93b6
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
5 changed files with 135 additions and 55 deletions

View file

@ -0,0 +1,18 @@
import http from '@/api/http';
export default (uuid: string, file: string, content: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(
`/api/client/servers/${uuid}/files/write`,
content,
{
params: { file },
headers: {
'Content-Type': 'text/plain',
},
},
)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -113,7 +113,7 @@ 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 pin-r pin-t z-50'}> <div className={'absolute pin-r pin-b z-50'}>
<div className={'m-3 rounded bg-neutral-900 border border-black'}> <div className={'m-3 rounded bg-neutral-900 border border-black'}>
<select <select
className={'input-dark'} className={'input-dark'}

View file

@ -2,37 +2,70 @@ import React, { lazy, useEffect, useState } from 'react';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import getFileContents from '@/api/server/files/getFileContents'; import getFileContents from '@/api/server/files/getFileContents';
import useRouter from 'use-react-router'; import useRouter from 'use-react-router';
import { Actions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import saveFileContents from '@/api/server/files/saveFileContents';
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor')); const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor'));
export default () => { export default () => {
const { location: { hash } } = useRouter(); const { location: { hash } } = useRouter();
const [ loading, setLoading ] = useState(true);
const [ content, setContent ] = useState(''); const [ content, setContent ] = useState('');
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
let ref: null| (() => Promise<string>) = null; const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const addError = useStoreState((state: Actions<ApplicationStore>) => state.flashes.addError);
let fetchFileContent: null | (() => Promise<string>) = null;
useEffect(() => { useEffect(() => {
getFileContents(uuid, hash.replace(/^#/, '')) getFileContents(uuid, hash.replace(/^#/, ''))
.then(setContent) .then(setContent)
.catch(error => console.error(error)); .catch(error => console.error(error))
.then(() => setLoading(false));
}, [ uuid, hash ]); }, [ uuid, hash ]);
const save = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!fetchFileContent) {
return;
}
setLoading(true);
fetchFileContent()
.then(content => {
return saveFileContents(uuid, hash.replace(/^#/, ''), content);
})
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'files' });
})
.then(() => setLoading(false));
};
return ( return (
<div className={'my-10 mb-4'}> <div className={'mt-10 mb-4'}>
<LazyAceEditor <FileManagerBreadcrumbs withinFileEditor={true}/>
initialModePath={hash.replace(/^#/, '') || 'plain_text'} <div className={'relative'}>
initialContent={content} <SpinnerOverlay visible={loading}/>
fetchContent={value => { <LazyAceEditor
ref = value; initialModePath={hash.replace(/^#/, '') || 'plain_text'}
}} initialContent={content}
onContentSaved={() => null} fetchContent={value => {
/> fetchFileContent = value;
}}
onContentSaved={() => null}
/>
</div>
{content &&
<div className={'flex justify-end mt-4'}> <div className={'flex justify-end mt-4'}>
<button className={'btn btn-primary btn-sm'}> <button className={'btn btn-primary btn-sm'} onClick={save}>
Save Content Save Content
</button> </button>
</div> </div>
}
</div> </div>
); );
}; };

View file

@ -0,0 +1,64 @@
import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import { NavLink } from 'react-router-dom';
export default ({ withinFileEditor }: { withinFileEditor?: boolean }) => {
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 setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
useEffect(() => {
const parts = window.location.hash.replace(/^#(\/)*/, '/').split('/');
if (withinFileEditor) {
setFile(parts.pop() || null);
}
setDirectory(parts.join('/'));
}, [ withinFileEditor, setDirectory ]);
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 (
<div className={'flex items-center text-sm mb-4 text-neutral-500'}>
/<span className={'px-1 text-neutral-300'}>home</span>/
<NavLink
to={`/server/${id}/files`}
onClick={() => setDirectory('/')}
className={'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#${crumb.path}`}
onClick={() => setDirectory(crumb.path!)}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
>
{crumb.name}
</NavLink>/
</React.Fragment>
:
<span key={index} className={'px-1 text-neutral-300'}>{crumb.name}</span>
))
}
{file &&
<React.Fragment>
<span className={'px-1 text-neutral-300'}>{file}</span>
</React.Fragment>
}
</div>
);
};

View file

@ -7,14 +7,15 @@ import { httpErrorToHuman } from '@/api/http';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FileObjectRow from '@/components/server/files/FileObjectRow'; import FileObjectRow from '@/components/server/files/FileObjectRow';
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
export default () => { export default () => {
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { contents: files, directory } = ServerContext.useStoreState(state => state.files); const { contents: files, directory } = ServerContext.useStoreState(state => state.files);
const { setDirectory, getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files); const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
const load = () => { useEffect(() => {
setLoading(true); setLoading(true);
clearFlashes(); clearFlashes();
@ -24,50 +25,14 @@ export default () => {
console.error(error.message, { error }); console.error(error.message, { error });
addError({ message: httpErrorToHuman(error), key: 'files' }); addError({ message: httpErrorToHuman(error), key: 'files' });
}); });
}; // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ directory ]);
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
.filter(directory => !!directory)
.map((directory, index, dirs) => {
if (index === dirs.length - 1) {
return { name: directory };
}
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
});
useEffect(() => load(), [ directory ]);
return ( return (
<div className={'my-10 mb-6'}> <div className={'my-10 mb-6'}>
<FlashMessageRender byKey={'files'} className={'mb-4'}/> <FlashMessageRender byKey={'files'} className={'mb-4'}/>
<React.Fragment> <React.Fragment>
<div className={'flex items-center text-sm mb-4 text-neutral-500'}> <FileManagerBreadcrumbs/>
/<span className={'px-1 text-neutral-300'}>home</span>/
<a
href={'#'}
onClick={() => setDirectory('/')}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
>
container
</a>/
{
breadcrumbs().map((crumb, index) => (
crumb.path ?
<React.Fragment key={index}>
<a
href={`#${crumb.path}`}
onClick={() => setDirectory(crumb.path!)}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
>
{crumb.name}
</a>/
</React.Fragment>
:
<span key={index} className={'px-1 text-neutral-300'}>{crumb.name}</span>
))
}
</div>
{ {
loading ? loading ?
<Spinner size={'large'} centered={true}/> <Spinner size={'large'} centered={true}/>