diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index c8a656c83c..faf3e18134 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -1,10 +1,10 @@ import objectID from 'bson-objectid'; import blobUtil from 'blob-util'; -import { reset } from 'redux-form'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide'; import { setProjectSavedTime } from './project'; +import { createError } from './ide'; function appendToFilename(filename, string) { @@ -36,106 +36,117 @@ export function updateFileContent(id, content) { }; } -export function createFile(formProps) { +export function createFile(file, parentId) { + return { + type: ActionTypes.CREATE_FILE, + ...file, + parentId + }; +} + +export function submitFile(formProps, files, parentId, projectId) { + if (projectId) { + const postParams = { + name: createUniqueName(formProps.name, parentId, files), + url: formProps.url, + content: formProps.content || '', + parentId, + children: [] + }; + return apiClient.post(`/projects/${projectId}/files`, postParams) + .then(response => ({ + file: response.data.updatedFile, + updatedAt: response.data.project.updatedAt + })); + } + const id = objectID().toHexString(); + const file = { + name: createUniqueName(formProps.name, parentId, files), + id, + _id: id, + url: formProps.url, + content: formProps.content || '', + children: [] + }; + return Promise.resolve({ + file + }); +} + +export function handleCreateFile(formProps) { return (dispatch, getState) => { const state = getState(); + const { files } = state; const { parentId } = state.ide; - if (state.project.id) { - const postParams = { - name: createUniqueName(formProps.name, parentId, state.files), - url: formProps.url, - content: formProps.content || '', - parentId, - children: [] - }; - apiClient.post(`/projects/${state.project.id}/files`, postParams) - .then((response) => { - dispatch({ - type: ActionTypes.CREATE_FILE, - ...response.data.updatedFile, - parentId - }); - dispatch(setProjectSavedTime(response.data.project.updatedAt)); - dispatch(closeNewFileModal()); - dispatch(reset('new-file')); - // dispatch({ - // type: ActionTypes.HIDE_MODAL - // }); - dispatch(setUnsavedChanges(true)); - }) - .catch((error) => { - const { response } = error; - dispatch({ - type: ActionTypes.ERROR, - error: response.data - }); - }); - } else { - const id = objectID().toHexString(); - dispatch({ - type: ActionTypes.CREATE_FILE, - name: createUniqueName(formProps.name, parentId, state.files), - id, - _id: id, - url: formProps.url, - content: formProps.content || '', - parentId, - children: [] + const projectId = state.project.id; + return new Promise((resolve) => { + submitFile(formProps, files, parentId, projectId).then((response) => { + const { file, updatedAt } = response; + dispatch(createFile(file, parentId)); + if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); + dispatch(closeNewFileModal()); + dispatch(setUnsavedChanges(true)); + resolve(); + }).catch((error) => { + const { response } = error; + dispatch(createError(response.data)); + resolve({ error }); }); - dispatch(reset('new-file')); - // dispatch({ - // type: ActionTypes.HIDE_MODAL - // }); - dispatch(setUnsavedChanges(true)); - dispatch(closeNewFileModal()); - } + }); + }; +} + +export function submitFolder(formProps, files, parentId, projectId) { + if (projectId) { + const postParams = { + name: createUniqueName(formProps.name, parentId, files), + content: '', + children: [], + parentId, + fileType: 'folder' + }; + return apiClient.post(`/projects/${projectId}/files`, postParams) + .then(response => ({ + file: response.data.updatedFile, + updatedAt: response.data.project.updatedAt + })); + } + const id = objectID().toHexString(); + const file = { + type: ActionTypes.CREATE_FILE, + name: createUniqueName(formProps.name, parentId, files), + id, + _id: id, + content: '', + // TODO pass parent id from File Tree + fileType: 'folder', + children: [] }; + return Promise.resolve({ + file + }); } -export function createFolder(formProps) { +export function handleCreateFolder(formProps) { return (dispatch, getState) => { const state = getState(); + const { files } = state; const { parentId } = state.ide; - if (state.project.id) { - const postParams = { - name: createUniqueName(formProps.name, parentId, state.files), - content: '', - children: [], - parentId, - fileType: 'folder' - }; - apiClient.post(`/projects/${state.project.id}/files`, postParams) - .then((response) => { - dispatch({ - type: ActionTypes.CREATE_FILE, - ...response.data.updatedFile, - parentId - }); - dispatch(setProjectSavedTime(response.data.project.updatedAt)); - dispatch(closeNewFolderModal()); - }) - .catch((error) => { - const { response } = error; - dispatch({ - type: ActionTypes.ERROR, - error: response.data - }); - }); - } else { - const id = objectID().toHexString(); - dispatch({ - type: ActionTypes.CREATE_FILE, - name: createUniqueName(formProps.name, parentId, state.files), - id, - _id: id, - content: '', - // TODO pass parent id from File Tree - parentId, - fileType: 'folder', - children: [] + const projectId = state.project.id; + return new Promise((resolve) => { + submitFolder(formProps, files, parentId, projectId).then((response) => { + const { file, updatedAt } = response; + dispatch(createFile(file, parentId)); + if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); + dispatch(closeNewFolderModal()); + dispatch(setUnsavedChanges(true)); + resolve(); + }).catch((error) => { + const { response } = error; + dispatch(createError(response.data)); + resolve({ error }); }); - dispatch(closeNewFolderModal()); - } + }); }; } diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 239dc6c757..e18d9d747c 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -278,3 +278,10 @@ export function stopSketch() { dispatch(stopVisualSketch()); }; } + +export function createError(error) { + return { + type: ActionTypes.ERROR, + error + }; +} diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index 712520dd53..abc48ecf88 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -1,69 +1,82 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { withTranslation } from 'react-i18next'; -import { domOnlyProps } from '../../../utils/reduxFormUtils'; +import React, { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Form, Field } from 'react-final-form'; +import { useDispatch } from 'react-redux'; +import { handleCreateFile } from '../actions/files'; +import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; import Button from '../../../common/Button'; -class NewFileForm extends React.Component { - constructor(props) { - super(props); - this.createFile = this.props.createFile.bind(this); - } +function NewFileForm() { + const fileNameInput = useRef(null); + const { t } = useTranslation(); + const dispatch = useDispatch(); - componentDidMount() { - this.fileName.focus(); + function onSubmit(formProps) { + return dispatch(handleCreateFile(formProps)); } - render() { - const { - fields: { name }, - handleSubmit, - } = this.props; - return ( -
{ - this.props.focusOnModal(); - handleSubmit(this.createFile)(data); - }} - > -
- - { - this.fileName = element; - }} - /> - -
- {name.touched && name.error && ( - {name.error} - )} -
- ); + function validate(formProps) { + const errors = {}; + + if (!formProps.name) { + errors.name = t('NewFileModal.EnterName'); + } else if (!formProps.name.match(CREATE_FILE_REGEX)) { + errors.name = t('NewFileModal.InvalidType'); + } + + return errors; } -} -NewFileForm.propTypes = { - fields: PropTypes.shape({ - name: PropTypes.objectOf(PropTypes.shape()).isRequired - }).isRequired, - handleSubmit: PropTypes.func.isRequired, - createFile: PropTypes.func.isRequired, - focusOnModal: PropTypes.func.isRequired, - t: PropTypes.func.isRequired -}; + useEffect(() => { + fileNameInput.current.focus(); + }); + + return ( +
+ {({ + handleSubmit, errors, touched, invalid, submitting + }) => ( + +
+ + {field => ( + + + + + )} + + +
+ {touched.name && errors.name && ( + {errors.name} + )} +
+ )} + + ); +} -export default withTranslation()(NewFileForm); +export default NewFileForm; diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index 3cb9904df3..c140244d57 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -1,15 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators, compose } from 'redux'; -import { reduxForm } from 'redux-form'; +import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; -import i18n from 'i18next'; import NewFileForm from './NewFileForm'; import { closeNewFileModal } from '../actions/ide'; -import { createFile } from '../actions/files'; -import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; - import ExitIcon from '../../../images/exit.svg'; @@ -59,7 +54,6 @@ class NewFileModal extends React.Component { @@ -68,36 +62,16 @@ class NewFileModal extends React.Component { } NewFileModal.propTypes = { - createFile: PropTypes.func.isRequired, closeNewFileModal: PropTypes.func.isRequired, t: PropTypes.func.isRequired }; -function validate(formProps) { - const errors = {}; - - if (!formProps.name) { - errors.name = i18n.t('NewFileModal.EnterName'); - } else if (!formProps.name.match(CREATE_FILE_REGEX)) { - errors.name = i18n.t('NewFileModal.InvalidType'); - } - - return errors; -} - function mapStateToProps() { return {}; } function mapDispatchToProps(dispatch) { - return bindActionCreators({ createFile, closeNewFileModal }, dispatch); + return bindActionCreators({ closeNewFileModal }, dispatch); } -export default withTranslation()(compose( - connect(mapStateToProps, mapDispatchToProps), - reduxForm({ - form: 'new-file', - fields: ['name'], - validate - }) -)(NewFileModal)); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(NewFileModal)); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index cc602489c6..44c272f237 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -1,72 +1,80 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { withTranslation } from 'react-i18next'; -import { domOnlyProps } from '../../../utils/reduxFormUtils'; - +import React, { useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Form, Field } from 'react-final-form'; +import { useDispatch } from 'react-redux'; import Button from '../../../common/Button'; +import { handleCreateFolder } from '../actions/files'; +function NewFolderForm() { + const folderNameInput = useRef(null); + useEffect(() => { + folderNameInput.current.focus(); + }); + const { t } = useTranslation(); + const dispatch = useDispatch(); -class NewFolderForm extends React.Component { - constructor(props) { - super(props); - this.createFolder = this.props.createFolder.bind(this); + function validate(formProps) { + const errors = {}; + if (!formProps.name) { + errors.name = t('NewFolderModal.EnterName'); + } else if (formProps.name.trim().length === 0) { + errors.name = t('NewFolderModal.EmptyName'); + } else if (formProps.name.match(/\.+/i)) { + errors.name = t('NewFolderModal.InvalidExtension'); + } + return errors; } - componentDidMount() { - this.fileName.focus(); + function onSubmit(formProps) { + return dispatch(handleCreateFolder(formProps)); } - render() { - const { - fields: { name }, - handleSubmit, - } = this.props; - return ( -
{ - handleSubmit(this.createFolder)(data); - }} - > -
- - { this.fileName = element; }} - {...domOnlyProps(name)} - /> - -
- {name.touched && name.error && ( - {name.error} - )} -
- ); - } + return ( +
+ {({ + handleSubmit, invalid, submitting, touched, errors + }) => ( + +
+ + {field => ( + + + + + )} + + +
+ {touched.name && errors.name && ( + {errors.name} + )} +
+ + )} + + ); } -NewFolderForm.propTypes = { - fields: PropTypes.shape({ - name: PropTypes.objectOf(PropTypes.shape()).isRequired - }).isRequired, - handleSubmit: PropTypes.func.isRequired, - createFolder: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired, - submitting: PropTypes.bool, - pristine: PropTypes.bool, - t: PropTypes.func.isRequired -}; -NewFolderForm.defaultProps = { - submitting: false, - pristine: true, -}; -export default withTranslation()(NewFolderForm); +export default NewFolderForm; diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index fce0ba50c2..ab66b2f1ae 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -1,10 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { reduxForm } from 'redux-form'; import { withTranslation } from 'react-i18next'; -import i18n from 'i18next'; import NewFolderForm from './NewFolderForm'; - import ExitIcon from '../../../images/exit.svg'; class NewFolderModal extends React.Component { @@ -43,7 +40,7 @@ class NewFolderModal extends React.Component {