diff --git a/client/components/__test__/FileNode.test.jsx b/client/components/__test__/FileNode.test.jsx new file mode 100644 index 00000000..b70ebf14 --- /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 8fb230cc..5edcf74d 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 e4abb9d5..2621d5c6 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 079e1826..4d560fc1 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); } } - handleFileNameChange(event) { - this.props.updateFileName(this.props.id, event.target.value); + handleFileClick = (event) => { + event.stopPropagation(); + const { isDeleting } = this.state; + const { id, setSelectedFile, name } = this.props; + if (name !== 'root' && !isDeleting) { + setSelectedFile(id); + } } - handleKeyPress(event) { + handleFileNameChange = (event) => { + const newName = event.target.value; + this.setUpdatedName(newName); + } + + handleFileNameBlur = () => { + this.validateFileName(); + this.hideEditFileName(); + } + + 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 ( - -
    • - -
    • -
    • - -
    • -
    • - -
    • - -
      - ); - } - })()} -
    • - -
    • -
    • - -
    • -
    -
    - ); - } - })()} - {(() => { // eslint-disable-line - if (this.props.children) { - return ( -
      - {this.props.children.map(this.renderChild)} + } + + { this.fileNameInput = element; }} + onBlur={this.handleFileNameBlur} + onKeyPress={this.handleKeyPress} + /> + +
      +
        + { isFolder && + +
      • + +
      • +
      • + +
      • + { 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 9a57ff4a..87b2c13d 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 && +
  • + +
  • + }