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_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
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) {
return {
type: ActionTypes.SHOW_FILE_OPTIONS,

View file

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

View file

@ -1,21 +1,33 @@
import React, { PropTypes } from 'react';
function NewFileForm(props) {
const { fields: { name }, handleSubmit } = props;
class NewFileForm extends React.Component {
constructor(props) {
super(props);
this.createFile = this.props.createFile.bind(this);
}
componentDidMount() {
this.refs.fileName.focus();
}
render() {
const { fields: { name }, handleSubmit } = this.props;
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>
<input
className="new-file-form__name-input"
id="name"
type="text"
placeholder="Name"
ref="fileName"
{...name}
/>
<input type="submit" value="Add File" aria-label="add file" />
</form>
);
}
}
NewFileForm.propTypes = {
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
// in which you can insert different content
// but for now, let's just make this work
class NewFileModal extends React.Component {
componentDidMount() {
document.getElementById('name').focus();
}
render() {
function NewFileModal(props) {
const modalClass = classNames({
modal: true,
'modal--reduced': !this.props.canUploadMedia
'modal--reduced': !props.canUploadMedia
});
return (
<section className={modalClass}>
<div className="modal-content">
<div className="modal__header">
<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" />
</button>
</div>
<NewFileForm {...this.props} />
<NewFileForm {...props} />
{(() => {
if (this.props.canUploadMedia) {
if (props.canUploadMedia) {
return (
<div>
<p className="modal__divider">OR</p>
@ -46,17 +42,14 @@ class NewFileModal extends React.Component {
</section>
);
}
}
NewFileModal.propTypes = {
closeModal: PropTypes.func.isRequired,
canUploadMedia: PropTypes.bool.isRequired
};
function mapStateToProps(state) {
return {
file: state.files
};
function mapStateToProps() {
return {};
}
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() {
const sidebarClass = classNames({
sidebar: true,
'sidebar--contracted': !this.props.isExpanded
'sidebar--contracted': !this.props.isExpanded,
'sidebar--project-options': this.props.projectOptionsVisible
});
return (
@ -35,12 +36,25 @@ class Sidebar extends React.Component {
</h3>
<div className="sidebar__icons">
<button
aria-label="add file"
aria-label="add file or folder"
className="sidebar__add"
onClick={this.props.newFile}
onClick={this.props.openProjectOptions}
onBlur={() => setTimeout(this.props.closeProjectOptions, 200)}
>
<InlineSVG src={downArrowUrl} />
</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
aria-label="collapse file navigation"
className="sidebar__contract"
@ -84,6 +98,7 @@ Sidebar.propTypes = {
files: PropTypes.array.isRequired,
setSelectedFile: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFile: PropTypes.func.isRequired,
collapseSidebar: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
@ -92,7 +107,10 @@ Sidebar.propTypes = {
deleteFile: PropTypes.func.isRequired,
showEditFileName: 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;

View file

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

View file

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

View file

@ -10,7 +10,9 @@ const initialState = {
modalIsVisible: false,
sidebarIsExpanded: true,
consoleIsExpanded: false,
preferencesIsVisible: false
preferencesIsVisible: false,
projectOptionsVisible: false,
newFolderModalVisible: false
};
const ide = (state = initialState, action) => {
@ -45,6 +47,14 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { preferencesIsVisible: false });
case ActionTypes.RESET_PROJECT:
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:
return state;
}

View file

@ -15,6 +15,11 @@
}
}
.modal-content-folder {
@extend .modal-content;
height: #{150 / $base-font-size}rem;
}
.modal__exit-button {
@extend %icon;
}
@ -25,11 +30,11 @@
margin-bottom: #{20 / $base-font-size}rem;
}
.new-file-form__name-label {
display: none;
.new-file-form__name-label, .new-folder-form__name-label {
@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;
}

View file

@ -126,6 +126,7 @@
.sidebar__icons {
display: flex;
align-items: center;
position: relative;
}
.sidebar__folder-icon {
@ -138,3 +139,15 @@
.sidebar__file-item-icon {
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;
}