Add support for creating a new file
This commit is contained in:
parent
d75073116f
commit
e784218645
6 changed files with 116 additions and 23 deletions
|
@ -59,7 +59,7 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ style, initialContent, initialModePath, fetchContent, onContentSaved }: Props) => {
|
export default ({ style, initialContent, initialModePath, fetchContent, onContentSaved }: Props) => {
|
||||||
const [ mode, setMode ] = useState('plain_text');
|
const [ mode, setMode ] = useState('ace/mode/plain_text');
|
||||||
|
|
||||||
const [ editor, setEditor ] = useState<Editor>();
|
const [ editor, setEditor ] = useState<Editor>();
|
||||||
const ref = useCallback(node => {
|
const ref = useCallback(node => {
|
||||||
|
|
|
@ -8,27 +8,33 @@ import { httpErrorToHuman } from '@/api/http';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import saveFileContents from '@/api/server/files/saveFileContents';
|
import saveFileContents from '@/api/server/files/saveFileContents';
|
||||||
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
|
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import FileNameModal from '@/components/server/files/FileNameModal';
|
||||||
|
|
||||||
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 { action } = useParams();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const { history, location: { hash } } = useRouter();
|
||||||
|
const [ loading, setLoading ] = useState(action === 'edit');
|
||||||
const [ content, setContent ] = useState('');
|
const [ content, setContent ] = useState('');
|
||||||
|
const [ modalVisible, setModalVisible ] = useState(false);
|
||||||
|
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const { id, uuid } = ServerContext.useStoreState(state => state.server.data!);
|
||||||
const addError = useStoreState((state: Actions<ApplicationStore>) => state.flashes.addError);
|
const addError = useStoreState((state: Actions<ApplicationStore>) => state.flashes.addError);
|
||||||
|
|
||||||
let fetchFileContent: null | (() => Promise<string>) = null;
|
let fetchFileContent: null | (() => Promise<string>) = null;
|
||||||
|
|
||||||
|
if (action !== 'new') {
|
||||||
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));
|
.then(() => setLoading(false));
|
||||||
}, [ uuid, hash ]);
|
}, [ uuid, hash ]);
|
||||||
|
}
|
||||||
|
|
||||||
const save = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const save = (name?: string) => {
|
||||||
if (!fetchFileContent) {
|
if (!fetchFileContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +42,15 @@ export default () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchFileContent()
|
fetchFileContent()
|
||||||
.then(content => {
|
.then(content => {
|
||||||
return saveFileContents(uuid, hash.replace(/^#/, ''), content);
|
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (name) {
|
||||||
|
history.push(`/server/${id}/files/edit#${hash.replace(/^#/, '')}/${name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -47,7 +61,15 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'mt-10 mb-4'}>
|
<div className={'mt-10 mb-4'}>
|
||||||
<FileManagerBreadcrumbs withinFileEditor={true}/>
|
<FileManagerBreadcrumbs withinFileEditor={true} isNewFile={action !== 'edit'}/>
|
||||||
|
<FileNameModal
|
||||||
|
visible={modalVisible}
|
||||||
|
onDismissed={() => setModalVisible(false)}
|
||||||
|
onFileNamed={(name) => {
|
||||||
|
setModalVisible(false);
|
||||||
|
save(name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className={'relative'}>
|
<div className={'relative'}>
|
||||||
<SpinnerOverlay visible={loading}/>
|
<SpinnerOverlay visible={loading}/>
|
||||||
<LazyAceEditor
|
<LazyAceEditor
|
||||||
|
@ -60,9 +82,15 @@ export default () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex justify-end mt-4'}>
|
<div className={'flex justify-end mt-4'}>
|
||||||
<button className={'btn btn-primary btn-sm'} onClick={save}>
|
{action === 'edit' ?
|
||||||
|
<button className={'btn btn-primary btn-sm'} onClick={() => save()}>
|
||||||
Save Content
|
Save Content
|
||||||
</button>
|
</button>
|
||||||
|
:
|
||||||
|
<button className={'btn btn-primary btn-sm'} onClick={() => setModalVisible(true)}>
|
||||||
|
Create File
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
export default ({ withinFileEditor }: { withinFileEditor?: boolean }) => {
|
interface Props {
|
||||||
|
withinFileEditor?: boolean;
|
||||||
|
isNewFile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
|
const { action } = useParams();
|
||||||
const [ file, setFile ] = useState<string | null>(null);
|
const [ file, setFile ] = useState<string | null>(null);
|
||||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||||
|
@ -11,12 +17,12 @@ export default ({ withinFileEditor }: { withinFileEditor?: boolean }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = window.location.hash.replace(/^#(\/)*/, '/').split('/');
|
const parts = window.location.hash.replace(/^#(\/)*/, '/').split('/');
|
||||||
|
|
||||||
if (withinFileEditor) {
|
if (withinFileEditor && !isNewFile) {
|
||||||
setFile(parts.pop() || null);
|
setFile(parts.pop() || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDirectory(parts.join('/'));
|
setDirectory(parts.join('/'));
|
||||||
}, [ withinFileEditor, setDirectory ]);
|
}, [ withinFileEditor, isNewFile, setDirectory ]);
|
||||||
|
|
||||||
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
||||||
.filter(directory => !!directory)
|
.filter(directory => !!directory)
|
||||||
|
@ -28,6 +34,9 @@ export default ({ withinFileEditor }: { withinFileEditor?: boolean }) => {
|
||||||
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
|
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (withinFileEditor)
|
||||||
|
console.log(breadcrumbs());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center text-sm mb-4 text-neutral-500'}>
|
<div className={'flex items-center text-sm mb-4 text-neutral-500'}>
|
||||||
/<span className={'px-1 text-neutral-300'}>home</span>/
|
/<span className={'px-1 text-neutral-300'}>home</span>/
|
||||||
|
|
|
@ -10,6 +10,7 @@ import FileObjectRow from '@/components/server/files/FileObjectRow';
|
||||||
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
|
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
|
||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||||
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
|
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
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))
|
||||||
|
@ -19,6 +20,7 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||||
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 { id } = ServerContext.useStoreState(state => state.server.data!);
|
||||||
const { contents: files, directory } = ServerContext.useStoreState(state => state.files);
|
const { contents: files, directory } = ServerContext.useStoreState(state => state.files);
|
||||||
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
|
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
|
||||||
|
|
||||||
|
@ -79,9 +81,9 @@ export default () => {
|
||||||
}
|
}
|
||||||
<div className={'flex justify-end mt-8'}>
|
<div className={'flex justify-end mt-8'}>
|
||||||
<NewDirectoryButton/>
|
<NewDirectoryButton/>
|
||||||
<button className={'btn btn-sm btn-primary'}>
|
<Link to={`/server/${id}/files/new${window.location.hash}`} className={'btn btn-sm btn-primary'}>
|
||||||
New File
|
New File
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
|
54
resources/scripts/components/server/files/FileNameModal.tsx
Normal file
54
resources/scripts/components/server/files/FileNameModal.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||||
|
import { Form, Formik, FormikActions } from 'formik';
|
||||||
|
import { object, string } from 'yup';
|
||||||
|
import Field from '@/components/elements/Field';
|
||||||
|
|
||||||
|
type Props = RequiredModalProps & {
|
||||||
|
onFileNamed: (name: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Values {
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ onFileNamed, onDismissed, ...props }: Props) => {
|
||||||
|
const submit = (values: Values, { setSubmitting }: FormikActions<Values>) => {
|
||||||
|
onFileNamed(values.fileName);
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
onSubmit={submit}
|
||||||
|
initialValues={{ fileName: '' }}
|
||||||
|
validationSchema={object().shape({
|
||||||
|
fileName: string().required().min(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ resetForm }) => (
|
||||||
|
<Modal
|
||||||
|
onDismissed={() => {
|
||||||
|
resetForm();
|
||||||
|
onDismissed();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Field
|
||||||
|
id={'fileName'}
|
||||||
|
name={'fileName'}
|
||||||
|
label={'File Name'}
|
||||||
|
description={'Enter the name that this file should be saved as.'}
|
||||||
|
/>
|
||||||
|
<div className={'mt-6 text-right'}>
|
||||||
|
<button className={'btn btn-primary btn-sm'}>
|
||||||
|
Create File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
|
@ -58,7 +58,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||||
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
|
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/files/edit`}
|
path={`${match.path}/files/:action(edit|new)`}
|
||||||
render={props => (
|
render={props => (
|
||||||
<SuspenseSpinner>
|
<SuspenseSpinner>
|
||||||
<FileEditContainer {...props as any}/>
|
<FileEditContainer {...props as any}/>
|
||||||
|
|
Loading…
Reference in a new issue