Skip to content

Commit

Permalink
Ability to rename a project
Browse files Browse the repository at this point in the history
closes #103
  • Loading branch information
meriadec committed Apr 28, 2017
1 parent a368633 commit 6d68d1e
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 29 deletions.
12 changes: 12 additions & 0 deletions app/actions/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
fileDialog,
fsReadFile,
fsAccess,
fsRename,
isValidDir,
rimraf,
} from 'helpers/fs'
Expand Down Expand Up @@ -125,3 +126,14 @@ export function updateProjectPreview (p, html) {
},
}
}

export function renameProject (oldPath, newPath) {
return async (dispatch) => {
await fsRename(oldPath, newPath)
dispatch({
type: 'PROJECT_RENAME',
payload: { oldPath, newPath },
})
dispatch(saveSettings())
}
}
2 changes: 2 additions & 0 deletions app/components/Modal/ConfirmModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ConfirmModal extends Component {
nopCTA,
onConfirm,
onCancel,
isConfirmDisabled,
className,
children,
...props
Expand All @@ -42,6 +43,7 @@ class ConfirmModal extends Component {
<Button
primary
onClick={onConfirm}
disabled={isConfirmDisabled}
>
{yepCTA}
</Button>
Expand Down
168 changes: 168 additions & 0 deletions app/components/ProjectsList/RenameModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { Component } from 'react'
import debounce from 'lodash/debounce'
import pathModule from 'path'
import IconCheck from 'react-icons/md/check-circle'
import IconChecking from 'react-icons/md/autorenew'
import IconError from 'react-icons/md/error'

import { alreadyExists } from 'helpers/fs'

import ConfirmModal from 'components/Modal/ConfirmModal'

class RenameModal extends Component {

state = {
newName: '',
oldName: '',
// unset / checking / valid / invalid
projectLocStatus: 'unset',
}

componentWillReceiveProps (nextProps) {
if (nextProps.isOpened && !this.props.isOpened) {
this.setState({
newName: pathModule.basename(nextProps.path),
oldName: pathModule.basename(nextProps.path),
})
}
}

componentDidUpdate (prevProps) {
if (this.props.isOpened && !prevProps.isOpened) {
this._inputName.focus()
}
}

handleConfirm = (e) => {
e && e.preventDefault()

const {
newName,
projectLocStatus,
} = this.state

if (projectLocStatus !== 'valid') { return }

const {
path,
} = this.props

const dir = path ? pathModule.dirname(path) : null
const fullPath = (newName && dir) ? pathModule.join(dir, newName) : null

this.props.onConfirm(fullPath)

}

handleChangeNewName = e => {
const newName = e.target.value
this.setState({
newName,
projectLocStatus: newName ? 'checking' : 'unset',
})
if (newName) {
this.debounceCheckName()
}
}

debounceCheckName = debounce(async () => {
const { path } = this.props
const { newName } = this.state
if (!newName) {
return this.setState({
projectLocStatus: 'unset',
})
}
const dir = pathModule.dirname(path)
const full = pathModule.join(dir, newName)
const exists = await alreadyExists(full)
this.setState({
projectLocStatus: exists ? 'invalid' : 'valid',
})
}, 250)

render () {

const {
isOpened,
onCancel,
path,
} = this.props

const {
newName,
oldName,
projectLocStatus,
} = this.state

const hasChanged = newName !== oldName
const dir = path ? pathModule.dirname(path) : null
const fullPath = (newName && dir) ? pathModule.join(dir, newName) : null

return (
<ConfirmModal
isOpened={isOpened}
yepCTA={'Rename project'}
nopCTA='Cancel'
onCancel={onCancel}
onConfirm={this.handleConfirm}
isConfirmDisabled={
!hasChanged
|| !newName
|| projectLocStatus !== 'valid'
}
>
<form onSubmit={this.handleConfirm}>
<h2 className='mb-20'>{'Rename project'}</h2>
<div className='flow-v-20'>
<div className='d-f ai-b'>
<div style={{ width: 150 }} className='fs-0'>
{'New name:'}
</div>
<div className='fg-1'>
<input
style={{ width: '100%' }}
ref={n => this._inputName = n}
className='fg-1'
value={newName}
onChange={this.handleChangeNewName}
placeholder='New name'
type='text'
/>
{fullPath && (
<div className='mt-10 t-small'>
{'Project will be renamed to: '}
<b className='c-white wb-ba'>
{fullPath}
</b>
</div>
)}
{projectLocStatus === 'checking' && (
<div className='t-small mt-10'>
<IconChecking className='rotating mr-5' />
{'Checking...'}
</div>
)}
{projectLocStatus === 'valid' && (
<div className='t-small mt-10 c-green'>
<IconCheck className='mr-5' />
{'Location is OK'}
</div>
)}
{projectLocStatus === 'invalid' && (
<div className='t-small mt-10 c-red'>
<IconError className='mr-5' />
{'Directory exists and is not empty'}
</div>
)}
</div>
</div>
</div>
</form>
</ConfirmModal>
)
}

}

export default RenameModal
93 changes: 70 additions & 23 deletions app/components/ProjectsList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,36 @@ import React, { Component } from 'react'
import path from 'path'
import { connect } from 'react-redux'
import IconClose from 'react-icons/md/close'
import IconEdit from 'react-icons/md/mode-edit'

import { openProject, removeProject } from 'actions/projects'
import {
openProject,
removeProject,
renameProject,
} from 'actions/projects'

import CheckBox from 'components/CheckBox'
import Preview from 'components/Preview'
import ConfirmModal from 'components/Modal/ConfirmModal'
import Tabbable from 'components/Tabbable'

import RenameModal from './RenameModal'

import './style.scss'

@connect(state => ({
projects: state.projects,
}), {
openProject,
removeProject,
renameProject,
})
class ProjectsList extends Component {

state = {
pathToDelete: null,
isModalOpened: false,
activePath: null,
isDeleteModalOpened: false,
isRenameModalOpened: false,
shouldDeleteFolder: false,
}

Expand All @@ -34,24 +43,43 @@ class ProjectsList extends Component {
e.preventDefault()
e.stopPropagation()
this.safeSetState({
pathToDelete: path,
isModalOpened: true,
activePath: path,
isDeleteModalOpened: true,
})
}

handleEditProjectName = path => e => {
e.preventDefault()
e.stopPropagation()
this.safeSetState({
activePath: path,
isRenameModalOpened: true,
})
}

handleConfirmRemove = () => {
const { pathToDelete, shouldDeleteFolder } = this.state
this.props.removeProject(pathToDelete, shouldDeleteFolder)
this.handleCloseModal()
const { activePath, shouldDeleteFolder } = this.state
this.props.removeProject(activePath, shouldDeleteFolder)
this.handleCloseDeleteModal()
}

handleCloseModal = () => this.safeSetState({
pathToDelete: null,
isModalOpened: false,
handleCloseDeleteModal = () => this.safeSetState({
activePath: null,
isDeleteModalOpened: false,
})

handleChangeShouldDelete = shouldDeleteFolder => this.setState({ shouldDeleteFolder })

handleCloseRenameModal = () => this.safeSetState({
activePath: null,
isRenameModalOpened: false,
})

handleRename = newPath => {
this.props.renameProject(this.state.activePath, newPath)
this.handleCloseRenameModal()
}

safeSetState = (...args) => {
if (this._isUnmounted) { return }
this.setState(...args)
Expand All @@ -65,45 +93,64 @@ class ProjectsList extends Component {
} = this.props

const {
isModalOpened,
isDeleteModalOpened,
isRenameModalOpened,
shouldDeleteFolder,
activePath,
} = this.state

return (
<div className='ProjectsList abs o-n'>
{projects.reverse().map((p) => (
<button
<div
className='ProjectItem'
key={p}
onClick={() => openProject(p.get('path'))}
tabIndex={0}
>
<Tabbable
className='ProjectItem--delete-btn'
onClick={this.handleRemoveProject(p.get('path'))}
>
<IconClose color='#fff' />
</Tabbable>
<div className='ProjectItem--preview-container'>
<Preview scaled html={p.get('html', null)} />
</div>
<div className='ProjectItem--label'>
{path.basename(p.get('path'))}
<Tabbable
onClick={() => openProject(p.get('path'))}
className='ProjectItem--preview-container-wrapper'
>
<div className='ProjectItem--preview-container'>
<Preview scaled html={p.get('html', null)} />
</div>
</Tabbable>
<div className='d-f ai-b pl-5 pr-5'>
<div className='ProjectItem--label'>
{path.basename(p.get('path'))}
</div>
<button
className='ProjectItem--edit-btn ml-5 pl-5 pr-5'
onClick={this.handleEditProjectName(p.get('path'))}
>
<IconEdit />
</button>
</div>
</button>
</div>
))}
<ConfirmModal
isOpened={isModalOpened}
isOpened={isDeleteModalOpened}
yepCTA={shouldDeleteFolder ? 'Remove from list and from disk' : 'Remove from list'}
nopCTA='Cancel'
onCancel={this.handleCloseModal}
onCancel={this.handleCloseDeleteModal}
onConfirm={this.handleConfirmRemove}
>
<h2 className='mb-20'>{'Remove project from list?'}</h2>
<CheckBox value={shouldDeleteFolder} onChange={this.handleChangeShouldDelete}>
{'Also remove folder and files from disk'}
</CheckBox>
</ConfirmModal>
<RenameModal
isOpened={isRenameModalOpened}
path={activePath}
onCancel={this.handleCloseRenameModal}
onConfirm={this.handleRename}
/>
</div>
)
}
Expand Down
Loading

0 comments on commit 6d68d1e

Please sign in to comment.