Square away saving of existing files
This commit is contained in:
parent
0dff732883
commit
78ccdf93b6
5 changed files with 135 additions and 55 deletions
18
resources/scripts/api/server/files/saveFileContents.ts
Normal file
18
resources/scripts/api/server/files/saveFileContents.ts
Normal 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);
|
||||
});
|
||||
};
|
|
@ -113,7 +113,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
|
|||
return (
|
||||
<EditorContainer style={style}>
|
||||
<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'}>
|
||||
<select
|
||||
className={'input-dark'}
|
||||
|
|
|
@ -2,37 +2,70 @@ import React, { lazy, useEffect, useState } from 'react';
|
|||
import { ServerContext } from '@/state/server';
|
||||
import getFileContents from '@/api/server/files/getFileContents';
|
||||
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'));
|
||||
|
||||
export default () => {
|
||||
const { location: { hash } } = useRouter();
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
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(() => {
|
||||
getFileContents(uuid, hash.replace(/^#/, ''))
|
||||
.then(setContent)
|
||||
.catch(error => console.error(error));
|
||||
.catch(error => console.error(error))
|
||||
.then(() => setLoading(false));
|
||||
}, [ 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 (
|
||||
<div className={'my-10 mb-4'}>
|
||||
<LazyAceEditor
|
||||
initialModePath={hash.replace(/^#/, '') || 'plain_text'}
|
||||
initialContent={content}
|
||||
fetchContent={value => {
|
||||
ref = value;
|
||||
}}
|
||||
onContentSaved={() => null}
|
||||
/>
|
||||
<div className={'mt-10 mb-4'}>
|
||||
<FileManagerBreadcrumbs withinFileEditor={true}/>
|
||||
<div className={'relative'}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
<LazyAceEditor
|
||||
initialModePath={hash.replace(/^#/, '') || 'plain_text'}
|
||||
initialContent={content}
|
||||
fetchContent={value => {
|
||||
fetchFileContent = value;
|
||||
}}
|
||||
onContentSaved={() => null}
|
||||
/>
|
||||
</div>
|
||||
{content &&
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -7,14 +7,15 @@ import { httpErrorToHuman } from '@/api/http';
|
|||
import { CSSTransition } from 'react-transition-group';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FileObjectRow from '@/components/server/files/FileObjectRow';
|
||||
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
|
||||
|
||||
export default () => {
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
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);
|
||||
clearFlashes();
|
||||
|
||||
|
@ -24,50 +25,14 @@ export default () => {
|
|||
console.error(error.message, { error });
|
||||
addError({ message: httpErrorToHuman(error), key: 'files' });
|
||||
});
|
||||
};
|
||||
|
||||
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 ]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ directory ]);
|
||||
|
||||
return (
|
||||
<div className={'my-10 mb-6'}>
|
||||
<FlashMessageRender byKey={'files'} className={'mb-4'}/>
|
||||
<React.Fragment>
|
||||
<div className={'flex items-center text-sm mb-4 text-neutral-500'}>
|
||||
/<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>
|
||||
<FileManagerBreadcrumbs/>
|
||||
{
|
||||
loading ?
|
||||
<Spinner size={'large'} centered={true}/>
|
||||
|
|
Loading…
Reference in a new issue