diff --git a/client/components/__test__/FileNode.test.jsx b/client/components/__test__/FileNode.test.jsx
new file mode 100644
index 0000000000..b70ebf14dd
--- /dev/null
+++ b/client/components/__test__/FileNode.test.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { FileNode } from '../../modules/IDE/components/FileNode';
+
+beforeAll(() => {});
+describe('', () => {
+ let component;
+ let props = {};
+
+ describe('with valid props', () => {
+ beforeEach(() => {
+ props = {
+ ...props,
+ id: '0',
+ children: [],
+ name: 'test.jsx',
+ fileType: 'dunno',
+ setSelectedFile: jest.fn(),
+ deleteFile: jest.fn(),
+ updateFileName: jest.fn(),
+ resetSelectedFile: jest.fn(),
+ newFile: jest.fn(),
+ newFolder: jest.fn(),
+ showFolderChildren: jest.fn(),
+ hideFolderChildren: jest.fn(),
+ canEdit: true,
+ authenticated: false
+ };
+ component = shallow();
+ });
+
+ describe('when changing name', () => {
+ let input;
+ let renameTriggerButton;
+ const changeName = (newFileName) => {
+ renameTriggerButton.simulate('click');
+ input.simulate('change', { target: { value: newFileName } });
+ input.simulate('blur');
+ };
+
+ beforeEach(() => {
+ input = component.find('.sidebar__file-item-input');
+ renameTriggerButton = component
+ .find('.sidebar__file-item-option')
+ .first();
+ component.setState({ isEditing: true });
+ });
+ it('should render', () => expect(component).toBeDefined());
+
+ // it('should debug', () => console.log(component.debug()));
+
+ describe('to a valid filename', () => {
+ const newName = 'newname.jsx';
+ beforeEach(() => changeName(newName));
+
+ it('should save the name', () => {
+ expect(props.updateFileName).toBeCalledWith(props.id, newName);
+ });
+ });
+
+ describe('to an empty filename', () => {
+ const newName = '';
+ beforeEach(() => changeName(newName));
+
+ it('should not save the name', () => {
+ expect(props.updateFileName).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js
index 8fb230cc97..5edcf74d44 100644
--- a/client/modules/IDE/actions/files.js
+++ b/client/modules/IDE/actions/files.js
@@ -136,10 +136,13 @@ export function createFolder(formProps) {
}
export function updateFileName(id, name) {
- return {
- type: ActionTypes.UPDATE_FILE_NAME,
- id,
- name
+ return (dispatch) => {
+ dispatch(setUnsavedChanges(true));
+ dispatch({
+ type: ActionTypes.UPDATE_FILE_NAME,
+ id,
+ name
+ });
};
}
diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js
index e4abb9d5ff..2621d5c659 100644
--- a/client/modules/IDE/actions/project.js
+++ b/client/modules/IDE/actions/project.js
@@ -133,6 +133,7 @@ export function saveProject(selectedFile = null, autosave = false) {
}
const formParams = Object.assign({}, state.project);
formParams.files = [...state.files];
+
if (selectedFile) {
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
fileToUpdate.content = selectedFile.content;
diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index 079e1826f3..4d560fc1fa 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -6,39 +6,29 @@ import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
-
-const downArrowUrl = require('../../../images/down-filled-triangle.svg');
-const folderRightUrl = require('../../../images/triangle-arrow-right.svg');
-const folderDownUrl = require('../../../images/triangle-arrow-down.svg');
-const fileUrl = require('../../../images/file.svg');
+import downArrowUrl from '../../../images/down-filled-triangle.svg';
+import folderRightUrl from '../../../images/triangle-arrow-right.svg';
+import folderDownUrl from '../../../images/triangle-arrow-down.svg';
+import fileUrl from '../../../images/file.svg';
export class FileNode extends React.Component {
constructor(props) {
super(props);
- this.renderChild = this.renderChild.bind(this);
- this.handleKeyPress = this.handleKeyPress.bind(this);
- this.handleFileNameChange = this.handleFileNameChange.bind(this);
- this.validateFileName = this.validateFileName.bind(this);
- this.handleFileClick = this.handleFileClick.bind(this);
- this.toggleFileOptions = this.toggleFileOptions.bind(this);
- this.hideFileOptions = this.hideFileOptions.bind(this);
- this.showEditFileName = this.showEditFileName.bind(this);
- this.hideEditFileName = this.hideEditFileName.bind(this);
- this.onBlurComponent = this.onBlurComponent.bind(this);
- this.onFocusComponent = this.onFocusComponent.bind(this);
this.state = {
isOptionsOpen: false,
isEditingName: false,
isFocused: false,
+ isDeleting: false,
+ updatedName: this.props.name
};
}
- onFocusComponent() {
+ onFocusComponent = () => {
this.setState({ isFocused: true });
}
- onBlurComponent() {
+ onBlurComponent = () => {
this.setState({ isFocused: false });
setTimeout(() => {
if (!this.state.isFocused) {
@@ -47,41 +37,96 @@ export class FileNode extends React.Component {
}, 200);
}
- handleFileClick(e) {
- e.stopPropagation();
- if (this.props.name !== 'root' && !this.isDeleting) {
- this.props.setSelectedFile(this.props.id);
+
+ setUpdatedName = (updatedName) => {
+ this.setState({ updatedName });
+ }
+
+ saveUpdatedFileName = () => {
+ const { updatedName } = this.state;
+ const { name, updateFileName, id } = this.props;
+
+ if (updatedName !== name) {
+ updateFileName(id, updatedName);
+ }
+ }
+
+ handleFileClick = (event) => {
+ event.stopPropagation();
+ const { isDeleting } = this.state;
+ const { id, setSelectedFile, name } = this.props;
+ if (name !== 'root' && !isDeleting) {
+ setSelectedFile(id);
}
}
- handleFileNameChange(event) {
- this.props.updateFileName(this.props.id, event.target.value);
+ handleFileNameChange = (event) => {
+ const newName = event.target.value;
+ this.setUpdatedName(newName);
+ }
+
+ handleFileNameBlur = () => {
+ this.validateFileName();
+ this.hideEditFileName();
}
- handleKeyPress(event) {
+ handleClickRename = () => {
+ this.setUpdatedName(this.props.name);
+ this.showEditFileName();
+ setTimeout(() => this.fileNameInput.focus(), 0);
+ setTimeout(() => this.hideFileOptions(), 0);
+ }
+
+ handleClickAddFile = () => {
+ this.props.newFile(this.props.id);
+ setTimeout(() => this.hideFileOptions(), 0);
+ }
+
+ handleClickAddFolder = () => {
+ this.props.newFolder(this.props.id);
+ setTimeout(() => this.hideFileOptions(), 0);
+ }
+
+ handleClickUploadFile = () => {
+ this.props.openUploadFileModal(this.props.id);
+ setTimeout(this.hideFileOptions, 0);
+ }
+
+ handleClickDelete = () => {
+ if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
+ this.setState({ isDeleting: true });
+ this.props.resetSelectedFile(this.props.id);
+ setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
+ }
+ }
+
+ handleKeyPress = (event) => {
if (event.key === 'Enter') {
this.hideEditFileName();
}
}
- validateFileName() {
- const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i);
- const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i);
- const hasPeriod = this.props.name.match(/\.+/);
- const newFileName = this.props.name;
+ validateFileName = () => {
+ const currentName = this.props.name;
+ const { updatedName } = this.state;
+ 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 = this.props.fileType === 'folder' && hasPeriod;
const notSameExtension = oldFileExtension && newFileExtension
&& oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase();
- const hasEmptyFilename = newFileName === '';
- const hasOnlyExtension = newFileExtension && newFileName === newFileExtension[0];
+ const hasEmptyFilename = updatedName.trim() === '';
+ const hasOnlyExtension = newFileExtension && updatedName.trim() === newFileExtension[0];
if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) {
- this.props.updateFileName(this.props.id, this.originalFileName);
+ this.setUpdatedName(currentName);
+ } else {
+ this.saveUpdatedFileName();
}
}
- toggleFileOptions(e) {
- e.preventDefault();
+ toggleFileOptions = (event) => {
+ event.preventDefault();
if (!this.props.canEdit) {
return;
}
@@ -93,26 +138,32 @@ export class FileNode extends React.Component {
}
}
- hideFileOptions() {
+ hideFileOptions = () => {
this.setState({ isOptionsOpen: false });
}
- showEditFileName() {
+ showEditFileName = () => {
this.setState({ isEditingName: true });
}
- hideEditFileName() {
+ hideEditFileName = () => {
this.setState({ isEditingName: false });
}
- renderChild(childId) {
- return (
-
-
-
- );
+ showFolderChildren = () => {
+ this.props.showFolderChildren(this.props.id);
+ }
+
+ hideFolderChildren = () => {
+ this.props.hideFolderChildren(this.props.id);
}
+ renderChild = childId => (
+
+
+
+ )
+
render() {
const itemClass = classNames({
'sidebar__root-item': this.props.name === 'root',
@@ -123,161 +174,132 @@ export class FileNode extends React.Component {
'sidebar__file-item--closed': this.props.isFolderClosed
});
+ const isFile = this.props.fileType === 'file';
+ const isFolder = this.props.fileType === 'folder';
+ const isRoot = this.props.name === 'root';
+
return (
- {(() => { // eslint-disable-line
- if (this.props.name !== 'root') {
- return (
-
-
- {(() => { // eslint-disable-line
- if (this.props.fileType === 'file') {
- return (
-
-
-
- );
- }
- return (
-
-
-
-
- );
- })()}
-
-
{ this.fileNameInput = element; }}
- onBlur={() => {
- this.validateFileName();
- this.hideEditFileName();
- }}
- onKeyPress={this.handleKeyPress}
- />
+ { !isRoot &&
+
+
+ { isFile &&
+
+
+
+ }
+ { isFolder &&
+
-
-
- {(() => { // eslint-disable-line
- if (this.props.fileType === 'folder') {
- return (
-
- -
-
-
- -
-
-
- -
-
-
-
-
- );
- }
- })()}
+
+
+ }
+
+
{ this.fileNameInput = element; }}
+ onBlur={this.handleFileNameBlur}
+ onKeyPress={this.handleKeyPress}
+ />
+
+
+
+ { isFolder &&
+
-
-
-
-
-
- );
- }
- })()}
- {(() => { // eslint-disable-line
- if (this.props.children) {
- return (
-
- {this.props.children.map(this.renderChild)}
+ { this.props.authenticated &&
+ -
+
+
+ }
+
+ }
+ -
+
+
+ -
+
+
- );
- }
- })()}
+
+
+ }
+ { this.props.children &&
+
+ {this.props.children.map(this.renderChild)}
+
+ }
);
}
@@ -300,18 +322,20 @@ FileNode.propTypes = {
showFolderChildren: PropTypes.func.isRequired,
hideFolderChildren: PropTypes.func.isRequired,
canEdit: PropTypes.bool.isRequired,
- openUploadFileModal: PropTypes.func.isRequired
+ openUploadFileModal: PropTypes.func.isRequired,
+ authenticated: PropTypes.bool.isRequired
};
FileNode.defaultProps = {
parentId: '0',
isSelectedFile: false,
- isFolderClosed: false
+ isFolderClosed: false,
};
function mapStateToProps(state, ownProps) {
// this is a hack, state is updated before ownProps
- return state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
+ const fileNode = state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
+ return Object.assign({}, fileNode, { authenticated: state.user.authenticated });
}
function mapDispatchToProps(dispatch) {
diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx
index 9a57ff4ad0..87b2c13d67 100644
--- a/client/modules/IDE/components/Sidebar.jsx
+++ b/client/modules/IDE/components/Sidebar.jsx
@@ -113,19 +113,22 @@ class Sidebar extends React.Component {
Create file
-
-
-
+ {
+ this.props.user.authenticated &&
+
+
+
+ }