diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index 2a381624ef..2a81545840 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -7,6 +7,8 @@ import { useSelector } from 'react-redux'; import * as IDEActions from '../actions/ide'; import * as FileActions from '../actions/files'; +import { showToast as showToastAction } from '../actions/toast'; +import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; import DownArrowIcon from '../../../images/down-filled-triangle.svg'; import FolderRightIcon from '../../../images/triangle-arrow-right.svg'; import FolderDownIcon from '../../../images/triangle-arrow-down.svg'; @@ -82,7 +84,8 @@ const FileNode = ({ canEdit, openUploadFileModal, authenticated, - onClickFile + onClickFile, + showToast }) => { const [isOptionsOpen, setIsOptionsOpen] = useState(false); const [isEditingName, setIsEditingName] = useState(false); @@ -186,23 +189,42 @@ const FileNode = ({ const oldFileExtension = currentName.match(/\.[0-9a-z]+$/i); const newFileExtension = updatedName.match(/\.[0-9a-z]+$/i); const hasPeriod = updatedName.match(/\.+/); - const hasNoExtension = oldFileExtension && !newFileExtension; - const hasExtensionIfFolder = fileType === 'folder' && hasPeriod; const notSameExtension = oldFileExtension && newFileExtension && oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); const hasEmptyFilename = updatedName.trim() === ''; - const hasOnlyExtension = - newFileExtension && updatedName.trim() === newFileExtension[0]; - if ( - hasEmptyFilename || - hasNoExtension || - hasOnlyExtension || - hasExtensionIfFolder - ) { + + // Check empty filename + if (hasEmptyFilename) { + if (fileType === 'folder') { + showToast(t('NewFolderModal.EnterName')); + } else { + showToast(t('NewFileModal.EnterName')); + } setUpdatedName(currentName); - } else if (notSameExtension) { + return; + } + + // Check folder specific rules + if (fileType === 'folder') { + if (hasPeriod) { + showToast(t('NewFolderModal.InvalidExtension')); + setUpdatedName(currentName); + return; + } + } + + // Check file specific rules + if (fileType === 'file') { + if (!updatedName.match(CREATE_FILE_REGEX)) { + showToast(t('NewFileModal.InvalidType')); + setUpdatedName(currentName); + return; + } + } + + if (notSameExtension) { const userResponse = window.confirm( 'Are you sure you want to change the file extension?' ); @@ -412,7 +434,8 @@ FileNode.propTypes = { canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, authenticated: PropTypes.bool.isRequired, - onClickFile: PropTypes.func + onClickFile: PropTypes.func, + showToast: PropTypes.func.isRequired }; FileNode.defaultProps = { @@ -433,7 +456,11 @@ function mapStateToProps(state, ownProps) { }); } -const mapDispatchToProps = { ...FileActions, ...IDEActions }; +const mapDispatchToProps = { + ...FileActions, + ...IDEActions, + showToast: showToastAction +}; const ConnectedFileNode = connect( mapStateToProps, diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx index 0c9fd6fb5a..9fec9bc43d 100644 --- a/client/modules/IDE/components/FileNode.unit.test.jsx +++ b/client/modules/IDE/components/FileNode.unit.test.jsx @@ -33,7 +33,7 @@ describe('', () => { const props = { ...extraProps, id: '0', - name: fileType === 'folder' ? 'afolder' : 'test.jsx', + name: fileType === 'folder' ? 'afolder' : 'test.js', fileType, canEdit: true, children: [], @@ -48,7 +48,8 @@ describe('', () => { showFolderChildren: jest.fn(), hideFolderChildren: jest.fn(), openUploadFileModal: jest.fn(), - setProjectName: jest.fn() + setProjectName: jest.fn(), + showToast: jest.fn() }; const mockFiles = [ @@ -96,11 +97,12 @@ describe('', () => { changeName(''); await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); await expectFileNameToBe(props.name); }); it('can change to a valid filename', async () => { - const newName = 'newname.jsx'; + const newName = 'newname.js'; const props = renderFileNode('file'); changeName(newName); @@ -118,6 +120,7 @@ describe('', () => { changeName(newName); await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); await expectFileNameToBe(props.name); }); @@ -125,7 +128,7 @@ describe('', () => { const mockConfirm = jest.fn(() => true); window.confirm = mockConfirm; - const newName = 'newname.gif'; + const newName = 'newname.json'; const props = renderFileNode('file'); changeName(newName); @@ -137,13 +140,25 @@ describe('', () => { await expectFileNameToBe(props.name); }); + it('cannot change to an invalid extension', async () => { + const newName = 'newname.obj'; + const props = renderFileNode('file'); + + changeName(newName); + + await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); + await expectFileNameToBe(props.name); + }); + it('cannot be just an extension', async () => { - const newName = '.jsx'; + const newName = '.js'; const props = renderFileNode('file'); changeName(newName); await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); await expectFileNameToBe(props.name); }); }); @@ -155,6 +170,7 @@ describe('', () => { changeName(''); await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); await expectFileNameToBe(props.name); }); @@ -171,12 +187,13 @@ describe('', () => { }); it('cannot have a file extension', async () => { - const newName = 'foldername.jsx'; + const newName = 'foldername.js'; const props = renderFileNode('folder'); changeName(newName); await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(props.showToast).toHaveBeenCalled(); await expectFileNameToBe(props.name); }); });