add new folder modal

This commit is contained in:
catarak 2016-08-29 23:23:10 -04:00
parent 7f5c970b6c
commit 9d3bcf2a15
14 changed files with 291 additions and 70 deletions

View file

@ -65,5 +65,10 @@ export const SET_LINT_WARNING = 'SET_LINT_WARNING';
export const SET_PREFERENCES = 'SET_PREFERENCES'; export const SET_PREFERENCES = 'SET_PREFERENCES';
export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT'; export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT';
export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS';
export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS';
export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL';
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
// eventually, handle errors more specifically and better // eventually, handle errors more specifically and better
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';

View file

@ -118,6 +118,51 @@ export function createFile(formProps) {
}; };
} }
export function createFolder(formProps) {
return (dispatch, getState) => {
const state = getState();
const rootFile = state.files.filter(file => file.name === 'root')[0];
if (state.project.id) {
const postParams = {
name: createUniqueName(formProps.name, state.files),
content: '',
parentId: rootFile.id,
fileType: 'folder'
};
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
.then(response => {
dispatch({
type: ActionTypes.CREATE_FILE,
...response.data,
parentId: rootFile.id
});
dispatch({
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
});
})
.catch(response => dispatch({
type: ActionTypes.ERROR,
error: response.data
}));
} else {
const id = objectID().toHexString();
dispatch({
type: ActionTypes.CREATE_FILE,
name: createUniqueName(formProps.name, state.files),
id,
_id: id,
content: '',
// TODO pass parent id from File Tree
parentId: rootFile.id,
fileType: 'folder'
});
dispatch({
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
});
}
};
}
export function showFileOptions(fileId) { export function showFileOptions(fileId) {
return { return {
type: ActionTypes.SHOW_FILE_OPTIONS, type: ActionTypes.SHOW_FILE_OPTIONS,

View file

@ -102,3 +102,27 @@ export function closePreferences() {
type: ActionTypes.CLOSE_PREFERENCES type: ActionTypes.CLOSE_PREFERENCES
}; };
} }
export function openProjectOptions() {
return {
type: ActionTypes.OPEN_PROJECT_OPTIONS
};
}
export function closeProjectOptions() {
return {
type: ActionTypes.CLOSE_PROJECT_OPTIONS
};
}
export function newFolder() {
return {
type: ActionTypes.SHOW_NEW_FOLDER_MODAL
};
}
export function closeNewFolderModal() {
return {
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
};
}

View file

@ -65,14 +65,14 @@ export class FileNode extends React.Component {
<div <div
className={itemClass} className={itemClass}
onClick={this.handleFileClick} onClick={this.handleFileClick}
onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 100)} onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 200)}
> >
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.name !== 'root') { if (this.props.name !== 'root') {
return ( return (
<div className="file-item__content"> <div className="file-item__content">
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.type === 'file') { if (this.props.fileType === 'file') {
return ( return (
<span className="sidebar__file-item-icon"> <span className="sidebar__file-item-icon">
<InlineSVG src={fileUrl} /> <InlineSVG src={fileUrl} />
@ -151,7 +151,7 @@ FileNode.propTypes = {
parentId: PropTypes.string, parentId: PropTypes.string,
children: PropTypes.array, children: PropTypes.array,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, fileType: PropTypes.string.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
isOptionsOpen: PropTypes.bool, isOptionsOpen: PropTypes.bool,
isEditingName: PropTypes.bool, isEditingName: PropTypes.bool,

View file

@ -1,21 +1,33 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
function NewFileForm(props) { class NewFileForm extends React.Component {
const { fields: { name }, handleSubmit } = props; constructor(props) {
super(props);
this.createFile = this.props.createFile.bind(this);
}
componentDidMount() {
this.refs.fileName.focus();
}
render() {
const { fields: { name }, handleSubmit } = this.props;
return ( return (
<form className="new-file-form" onSubmit={handleSubmit(props.createFile.bind(this))}> <form className="new-file-form" onSubmit={handleSubmit(this.createFile)}>
<label className="new-file-form__name-label" htmlFor="name">Name:</label> <label className="new-file-form__name-label" htmlFor="name">Name:</label>
<input <input
className="new-file-form__name-input" className="new-file-form__name-input"
id="name" id="name"
type="text" type="text"
placeholder="Name" placeholder="Name"
ref="fileName"
{...name} {...name}
/> />
<input type="submit" value="Add File" aria-label="add file" /> <input type="submit" value="Add File" aria-label="add file" />
</form> </form>
); );
} }
}
NewFileForm.propTypes = { NewFileForm.propTypes = {
fields: PropTypes.shape({ fields: PropTypes.shape({

View file

@ -12,27 +12,23 @@ import FileUploader from './FileUploader';
// At some point this will probably be generalized to a generic modal // At some point this will probably be generalized to a generic modal
// in which you can insert different content // in which you can insert different content
// but for now, let's just make this work // but for now, let's just make this work
class NewFileModal extends React.Component { function NewFileModal(props) {
componentDidMount() {
document.getElementById('name').focus();
}
render() {
const modalClass = classNames({ const modalClass = classNames({
modal: true, modal: true,
'modal--reduced': !this.props.canUploadMedia 'modal--reduced': !props.canUploadMedia
}); });
return ( return (
<section className={modalClass}> <section className={modalClass}>
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Add File</h2> <h2 className="modal__title">Add File</h2>
<button className="modal__exit-button" onClick={this.props.closeModal}> <button className="modal__exit-button" onClick={props.closeModal}>
<InlineSVG src={exitUrl} alt="Close New File Modal" /> <InlineSVG src={exitUrl} alt="Close New File Modal" />
</button> </button>
</div> </div>
<NewFileForm {...this.props} /> <NewFileForm {...props} />
{(() => { {(() => {
if (this.props.canUploadMedia) { if (props.canUploadMedia) {
return ( return (
<div> <div>
<p className="modal__divider">OR</p> <p className="modal__divider">OR</p>
@ -46,17 +42,14 @@ class NewFileModal extends React.Component {
</section> </section>
); );
} }
}
NewFileModal.propTypes = { NewFileModal.propTypes = {
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
canUploadMedia: PropTypes.bool.isRequired canUploadMedia: PropTypes.bool.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps() {
return { return {};
file: state.files
};
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {

View file

@ -0,0 +1,36 @@
import React, { PropTypes } from 'react';
class NewFolderForm extends React.Component {
constructor(props) {
super(props);
this.createFolder = this.props.createFolder.bind(this);
}
render() {
const { fields: { name }, handleSubmit } = this.props;
return (
<form className="new-folder-form" onSubmit={handleSubmit(this.createFolder)}>
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
<input
className="new-folder-form__name-input"
id="name"
type="text"
placeholder="Name"
ref="fileName"
{...name}
/>
<input type="submit" value="Add Folder" aria-label="add folder" />
</form>
);
}
}
NewFolderForm.propTypes = {
fields: PropTypes.shape({
name: PropTypes.string.isRequired
}).isRequired,
handleSubmit: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired
};
export default NewFolderForm;

View file

@ -0,0 +1,31 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import NewFolderForm from './NewFolderForm';
function NewFolderModal(props) {
return (
<section className="modal">
<div className="modal-content-folder">
<div className="modal__header">
<h2 className="modal__title">Add Folder</h2>
<button className="modal__exit-button" onClick={props.closeModal}>
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
</button>
</div>
<NewFolderForm {...props} />
</div>
</section>
);
}
NewFolderModal.propTypes = {
closeModal: PropTypes.func.isRequired
};
export default reduxForm({
form: 'new-folder',
fields: ['name']
})(NewFolderModal);

View file

@ -21,7 +21,8 @@ class Sidebar extends React.Component {
render() { render() {
const sidebarClass = classNames({ const sidebarClass = classNames({
sidebar: true, sidebar: true,
'sidebar--contracted': !this.props.isExpanded 'sidebar--contracted': !this.props.isExpanded,
'sidebar--project-options': this.props.projectOptionsVisible
}); });
return ( return (
@ -35,12 +36,25 @@ class Sidebar extends React.Component {
</h3> </h3>
<div className="sidebar__icons"> <div className="sidebar__icons">
<button <button
aria-label="add file" aria-label="add file or folder"
className="sidebar__add" className="sidebar__add"
onClick={this.props.newFile} onClick={this.props.openProjectOptions}
onBlur={() => setTimeout(this.props.closeProjectOptions, 200)}
> >
<InlineSVG src={downArrowUrl} /> <InlineSVG src={downArrowUrl} />
</button> </button>
<ul className="sidebar__project-options">
<li>
<a onClick={this.props.newFolder} >
Add Folder
</a>
</li>
<li>
<a onClick={this.props.newFile} >
Add File
</a>
</li>
</ul>
<button <button
aria-label="collapse file navigation" aria-label="collapse file navigation"
className="sidebar__contract" className="sidebar__contract"
@ -84,6 +98,7 @@ Sidebar.propTypes = {
files: PropTypes.array.isRequired, files: PropTypes.array.isRequired,
setSelectedFile: PropTypes.func.isRequired, setSelectedFile: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired, isExpanded: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFile: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired,
collapseSidebar: PropTypes.func.isRequired, collapseSidebar: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired, expandSidebar: PropTypes.func.isRequired,
@ -92,7 +107,10 @@ Sidebar.propTypes = {
deleteFile: PropTypes.func.isRequired, deleteFile: PropTypes.func.isRequired,
showEditFileName: PropTypes.func.isRequired, showEditFileName: PropTypes.func.isRequired,
hideEditFileName: PropTypes.func.isRequired, hideEditFileName: PropTypes.func.isRequired,
updateFileName: PropTypes.func.isRequired updateFileName: PropTypes.func.isRequired,
openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired
}; };
export default Sidebar; export default Sidebar;

View file

@ -6,6 +6,7 @@ import Toolbar from '../components/Toolbar';
import TextOutput from '../components/TextOutput'; import TextOutput from '../components/TextOutput';
import Preferences from '../components/Preferences'; import Preferences from '../components/Preferences';
import NewFileModal from '../components/NewFileModal'; import NewFileModal from '../components/NewFileModal';
import NewFolderModal from '../components/NewFolderModal';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import Console from '../components/Console'; import Console from '../components/Console';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -177,6 +178,10 @@ class IDEView extends React.Component {
showEditFileName={this.props.showEditFileName} showEditFileName={this.props.showEditFileName}
hideEditFileName={this.props.hideEditFileName} hideEditFileName={this.props.hideEditFileName}
updateFileName={this.props.updateFileName} updateFileName={this.props.updateFileName}
projectOptionsVisible={this.props.ide.projectOptionsVisible}
openProjectOptions={this.props.openProjectOptions}
closeProjectOptions={this.props.closeProjectOptions}
newFolder={this.props.newFolder}
/> />
<SplitPane <SplitPane
split="vertical" split="vertical"
@ -258,6 +263,17 @@ class IDEView extends React.Component {
} }
return ''; return '';
})()} })()}
{(() => {
if (this.props.ide.newFolderModalVisible) {
return (
<NewFolderModal
closeModal={this.props.closeNewFolderModal}
createFolder={this.props.createFolder}
/>
);
}
return '';
})()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.location.pathname.match(/sketches$/)) { if (this.props.location.pathname.match(/sketches$/)) {
return ( return (
@ -304,7 +320,9 @@ IDEView.propTypes = {
modalIsVisible: PropTypes.bool.isRequired, modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired preferencesIsVisible: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFolderModalVisible: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
@ -370,7 +388,12 @@ IDEView.propTypes = {
updateFileName: PropTypes.func.isRequired, updateFileName: PropTypes.func.isRequired,
showEditProjectName: PropTypes.func.isRequired, showEditProjectName: PropTypes.func.isRequired,
hideEditProjectName: PropTypes.func.isRequired, hideEditProjectName: PropTypes.func.isRequired,
logoutUser: PropTypes.func.isRequired logoutUser: PropTypes.func.isRequired,
openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired,
closeNewFolderModal: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -43,7 +43,7 @@ function initialState() {
id: r, id: r,
_id: r, _id: r,
children: [a, b, c], children: [a, b, c],
type: 'folder' fileType: 'folder'
}, },
{ {
name: 'sketch.js', name: 'sketch.js',
@ -51,21 +51,21 @@ function initialState() {
id: a, id: a,
_id: a, _id: a,
isSelected: true, isSelected: true,
type: 'file' fileType: 'file'
}, },
{ {
name: 'index.html', name: 'index.html',
content: defaultHTML, content: defaultHTML,
id: b, id: b,
_id: b, _id: b,
type: 'file' fileType: 'file'
}, },
{ {
name: 'style.css', name: 'style.css',
content: defaultCSS, content: defaultCSS,
id: c, id: c,
_id: c, _id: c,
type: 'file' fileType: 'file'
}]; }];
} }
@ -105,7 +105,13 @@ const files = (state, action) => {
} }
return file; return file;
}); });
return [...newState, { name: action.name, id: action.id, _id: action._id, content: action.content, url: action.url, type: 'file' }]; return [...newState,
{ name: action.name,
id: action.id,
_id: action._id,
content: action.content,
url: action.url,
fileType: action.fileType || 'file' }];
} }
case ActionTypes.SHOW_FILE_OPTIONS: case ActionTypes.SHOW_FILE_OPTIONS:
return state.map(file => { return state.map(file => {

View file

@ -10,7 +10,9 @@ const initialState = {
modalIsVisible: false, modalIsVisible: false,
sidebarIsExpanded: true, sidebarIsExpanded: true,
consoleIsExpanded: false, consoleIsExpanded: false,
preferencesIsVisible: false preferencesIsVisible: false,
projectOptionsVisible: false,
newFolderModalVisible: false
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
@ -45,6 +47,14 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { preferencesIsVisible: false }); return Object.assign({}, state, { preferencesIsVisible: false });
case ActionTypes.RESET_PROJECT: case ActionTypes.RESET_PROJECT:
return initialState; return initialState;
case ActionTypes.OPEN_PROJECT_OPTIONS:
return Object.assign({}, state, { projectOptionsVisible: true });
case ActionTypes.CLOSE_PROJECT_OPTIONS:
return Object.assign({}, state, { projectOptionsVisible: false });
case ActionTypes.SHOW_NEW_FOLDER_MODAL:
return Object.assign({}, state, { newFolderModalVisible: true });
case ActionTypes.CLOSE_NEW_FOLDER_MODAL:
return Object.assign({}, state, { newFolderModalVisible: false });
default: default:
return state; return state;
} }

View file

@ -15,6 +15,11 @@
} }
} }
.modal-content-folder {
@extend .modal-content;
height: #{150 / $base-font-size}rem;
}
.modal__exit-button { .modal__exit-button {
@extend %icon; @extend %icon;
} }
@ -25,11 +30,11 @@
margin-bottom: #{20 / $base-font-size}rem; margin-bottom: #{20 / $base-font-size}rem;
} }
.new-file-form__name-label { .new-file-form__name-label, .new-folder-form__name-label {
display: none; @extend %hidden-element;
} }
.new-file-form__name-input { .new-file-form__name-input, .new-folder-form__name-input {
margin-right: #{10 / $base-font-size}rem; margin-right: #{10 / $base-font-size}rem;
} }

View file

@ -126,6 +126,7 @@
.sidebar__icons { .sidebar__icons {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative;
} }
.sidebar__folder-icon { .sidebar__folder-icon {
@ -138,3 +139,15 @@
.sidebar__file-item-icon { .sidebar__file-item-icon {
margin-right: #{5 / $base-font-size}rem; margin-right: #{5 / $base-font-size}rem;
} }
.sidebar__project-options {
@extend %modal;
display: none;
position: absolute;
.sidebar--project-options & {
display: block;
}
top: 100%;
padding: #{8 / $base-font-size}rem;
width: #{110 / $base-font-size}rem;
}