From db778e9c381964b387d7f6e3e0eee60d1ac910a5 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Tue, 22 Nov 2016 13:16:11 -0800 Subject: [PATCH 01/17] Added template selection UI --- .../components/buttons/LandingButton.jsx | 1 + .../scripts/components/pages/LandingPage.jsx | 16 +- .../components/pages/TemplatesPage.jsx | 244 ++++++++++++++++++ web/src/scripts/containers/Landing.jsx | 52 +++- 4 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 web/src/scripts/components/pages/TemplatesPage.jsx diff --git a/web/src/scripts/components/buttons/LandingButton.jsx b/web/src/scripts/components/buttons/LandingButton.jsx index 755018d..c556ed4 100644 --- a/web/src/scripts/components/buttons/LandingButton.jsx +++ b/web/src/scripts/components/buttons/LandingButton.jsx @@ -35,6 +35,7 @@ const defaultStyle = { cursor: 'default', flex: '0 0 35px', fontWeight: '400', + whiteSpace: 'pre', } const activeStyle = { diff --git a/web/src/scripts/components/pages/LandingPage.jsx b/web/src/scripts/components/pages/LandingPage.jsx index 526c292..ceabb3a 100644 --- a/web/src/scripts/components/pages/LandingPage.jsx +++ b/web/src/scripts/components/pages/LandingPage.jsx @@ -44,13 +44,13 @@ const topStyle = { const bottomStyle = { display: 'flex', - flex: '0 0 100px', + flex: '0 0 auto', backgroundColor: 'rgb(250,250,250)', borderTop: '1px solid #E7E7E7', flexDirection: 'column', justifyContent: 'center', alignItems: 'stretch', - padding: '0px 100px', + padding: '20px 100px', } const projectListStyle = { @@ -71,9 +71,13 @@ const logoWrapperStyle = { alignItems: 'center', } -const LandingPage = ({ onOpen, onCreateNew, recentProjects }) => { +const buttonDividerStyle = { + height: 15, +} + +const LandingPage = ({ onOpen, onCreateNew, recentProjects, onViewTemplates }) => { return ( -
+
@@ -107,6 +111,10 @@ const LandingPage = ({ onOpen, onCreateNew, recentProjects }) => { New Project +
+ + Project Templates... +
) diff --git a/web/src/scripts/components/pages/TemplatesPage.jsx b/web/src/scripts/components/pages/TemplatesPage.jsx new file mode 100644 index 0000000..df1149c --- /dev/null +++ b/web/src/scripts/components/pages/TemplatesPage.jsx @@ -0,0 +1,244 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import React, { Component, PropTypes } from 'react' + +import DecoLogo from '../display/DecoLogo' +import LandingButton from '../buttons/LandingButton' +import ProjectListItem from '../buttons/ProjectListItem' + +const reactNative = [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "0.36.0", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" + } +] + +const exponent = [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" + }, + { + "id": "tabs", + "name": "Tab Navigation", + "description": "The Tab Navigation project template includes several example screens.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_tabs.png" + } +] + +const styles = { + container: { + position: 'absolute', + width: '100%', + height: '100%', + backgroundColor: "#ffffff", + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + }, + top: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + minHeight: 0, + minWidth: 0, + }, + bottom: { + display: 'flex', + flex: '0 0 auto', + backgroundColor: 'rgb(250,250,250)', + borderTop: '1px solid #E7E7E7', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: '20px', + }, + paneContainer: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'row', + alignItems: 'stretch', + minHeight: 0, + minWidth: 0, + borderTop: '1px solid #E7E7E7', + }, + categoriesPane: { + flex: '0 0 180px', + overflowY: 'auto', + borderRight: '1px solid rgba(0,0,0,0.05)', + }, + category: { + padding: '20px 25px', + fontSize: 14, + fontWeight: 300, + borderBottom: '1px solid rgba(0,0,0,0.05)', + }, + logoContainer: { + flex: '0 0 auto', + padding: '35px 0px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + WebkitAppRegion: 'drag', + }, + templatesPane: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + minHeight: 0, + minWidth: 0, + paddingLeft: 20, + }, + template: { + padding: 30, + }, + templateTitle: { + paddingBottom: 10, + fontSize: 14, + fontWeight: 300, + }, + templateImage: { + width: 150, + height: 150, + backgroundColor: 'rgba(0,0,0,0.05)', + boxShadow: '0 2px 4px rgba(0,0,0,0.3)', + backgroundSize: 'cover', + }, +} + +styles.categoryActive = { + ...styles.category, + fontWeight: 'bold', + backgroundColor: 'rgba(0,0,0,0.05)', +} + +styles.templateActive = { + ...styles.template, + backgroundColor: 'rgb(79,139,229)', + color: 'white', +} + +const CATEGORIES = [ + 'React Native', + 'Exponent', +] + +const TEMPLATES_FOR_CATEGORY = { + 'React Native': reactNative, + 'Exponent': exponent, +} + +export default class TemplatesPage extends Component { + + state = { + selectedCategory: CATEGORIES[0], + selectedTemplateIndex: null, + } + + onSelectCategory = (title) => this.setState({ + selectedCategory: title, + selectedTemplateIndex: null, + }) + + onSelectTemplate = (index) => this.setState({ + selectedTemplateIndex: index, + }) + + renderCategory = (title, i) => { + const {selectedCategory} = this.state + + return ( +
+ {title} +
+ ) + } + + renderTemplate = ({name, iconUrl}, i) => { + const {selectedTemplateIndex} = this.state + + return ( +
+
+ {name} +
+
+
+ ) + } + + onCreateProject = () => { + const {onCreateProject} = this.props + const {selectedCategory, selectedTemplateIndex} = this.state + + // Make sure we've picked a template + if (selectedTemplateIndex === null) return + + const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] + + onCreateProject(selectedCategory, template) + } + + render() { + const {onBack} = this.props + const {selectedCategory} = this.state + + return ( +
+
+
+ +
+
+
+ {CATEGORIES.map(this.renderCategory)} +
+
+ {TEMPLATES_FOR_CATEGORY[selectedCategory].map(this.renderTemplate)} +
+
+
+
+ + Back + + + Create Project + +
+
+ ) + } +} diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index 8ca5eb1..c97dfae 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -24,15 +24,15 @@ import { resizeWindow, } from '../actions/uiActions' import RecentProjectUtils from '../utils/RecentProjectUtils' import LandingPage from '../components/pages/LandingPage' +import TemplatesPage from '../components/pages/TemplatesPage' class Landing extends Component { - constructor(props) { - super(props) - this.state = { - recentProjects: RecentProjectUtils.getProjectPaths(), - } + state = { + recentProjects: RecentProjectUtils.getProjectPaths(), + page: 'landing', } + componentWillMount() { this.props.dispatch(resizeWindow({ width: 640, @@ -40,7 +40,33 @@ class Landing extends Component { center: true, })) } - render() { + + onViewLanding = () => this.setState({page: 'landing'}) + + onViewTemplates = () => this.setState({page: 'templates'}) + + onCreateProject = (category, template) => { + const {dispatch} = this.props + + console.log('Creating project', category, template) + + if (category === 'React Native') { + dispatch(createProject()) + } else { + // ... exponent stuff ... + } + } + + renderTemplatesPage = () => { + return ( + + ) + } + + renderLandingPage = () => { const {recentProjects} = this.state return ( @@ -51,9 +77,21 @@ class Landing extends Component { }} onCreateNew={() => { this.props.dispatch(createProject()) - }} /> + }} + onViewTemplates={this.onViewTemplates} + /> ) } + + render() { + const {page} = this.state + + if (page === 'templates') { + return this.renderTemplatesPage() + } else { + return this.renderLandingPage() + } + } } export default connect()(Landing) From 0f48fd2fd0820691c3846229bb3ca71f9edaf0bf Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Tue, 22 Nov 2016 19:23:03 -0800 Subject: [PATCH 02/17] Detect project template and update toolbars with template-specific options --- desktop/src/menu/menuHandler.js | 17 ++-- desktop/src/menu/templateBuilder.js | 99 +++++++++---------- web/src/scripts/actions/fileActions.js | 22 +++-- web/src/scripts/containers/Root/Router.jsx | 8 +- .../scripts/containers/WorkspaceToolbar.jsx | 90 +++++++++++++---- web/src/scripts/reducers/fileReducer.js | 3 + web/src/scripts/utils/ProjectTemplateUtils.js | 21 ++++ 7 files changed, 166 insertions(+), 94 deletions(-) create mode 100644 web/src/scripts/utils/ProjectTemplateUtils.js diff --git a/desktop/src/menu/menuHandler.js b/desktop/src/menu/menuHandler.js index e866087..7f3dfcd 100644 --- a/desktop/src/menu/menuHandler.js +++ b/desktop/src/menu/menuHandler.js @@ -21,18 +21,13 @@ const Menu = require('electron').Menu const TemplateBuilder = require('./templateBuilder.js') class MenuHandler { - instantiateTemplate() { - var template = new TemplateBuilder(process.platform).makeTemplate() - this._template = template - this._menu = Menu.buildFromTemplate(this._template) - Menu.setApplicationMenu(this._menu) - } + instantiateTemplate(options = {}) { + const builder = new TemplateBuilder({platform: process.platform, ...options}) + const template = builder.makeTemplate() + const menu = Menu.buildFromTemplate(template) - get menu() { - return this._menu + Menu.setApplicationMenu(menu) } } -const handler = new MenuHandler() - -module.exports = handler +module.exports = new MenuHandler() diff --git a/desktop/src/menu/templateBuilder.js b/desktop/src/menu/templateBuilder.js index 9633118..37bba3b 100644 --- a/desktop/src/menu/templateBuilder.js +++ b/desktop/src/menu/templateBuilder.js @@ -71,7 +71,36 @@ import { const Logger = require('../log/logger') -const TemplateBuilder = function(platform) { +const restartPackager = () => PackagerController.runPackager(null) + +const buildProject = () => BuildController.buildIOS() + +const cleanProject = () => { + try { + const root = fileHandler.getWatchedPath() + if (root) { + projectHandler.cleanBuildDir(root) + } + } catch (e) { + Logger.error(e) + } +} + +const reloadSimulator = () => { + processHandler.onHardReloadSimulator({}, (response) => { + if (response.type == ERROR) { + Logger.error(response.message) + } + }) +} + +const reloadApplicationUI = (item, focusedWindow) => { + if (focusedWindow) { + focusedWindow.reload() + } +} + +const TemplateBuilder = function({platform, projectTemplateType}) { this.fileMenu = { label: 'File', @@ -292,60 +321,20 @@ const TemplateBuilder = function(platform) { this.toolsMenu = { label: 'Tools', - submenu: [{ - label: 'Restart Packager', - click: function() { - PackagerController.runPackager(null) - } - }, { - type: 'separator', - }, { - label: 'Build Native Modules', - accelerator: 'Command+B', - click: function() { - BuildController.buildIOS() - } - }, { - label: 'Clean', - accelerator: 'CommandOrCtrl+Alt+K', - click: function() { - try { - const root = fileHandler.getWatchedPath() - if (root) { - projectHandler.cleanBuildDir(root) - } - } catch (e) { - Logger.error(e) - } - }, - }, { - type: 'separator' - }, { - label: 'Run/Reload Simulator', - accelerator: 'CmdOrCtrl+R', - click: function() { - processHandler.onHardReloadSimulator({}, (response) => { - if (response.type == ERROR) { - Logger.error(response.message) - } - }) - } - }, ] - } - - if (global.__DEV__) { - this.toolsMenu.submenu.push({ - type: 'separator' - }) - this.toolsMenu.submenu.push({ - label: 'Reload Last Save', - accelerator: 'CmdOrCtrl+Shift+R', - click: function(item, focusedWindow) { - if (focusedWindow) { - focusedWindow.reload() - } - } - }) + submenu: [ + {label: 'Restart Packager', click: restartPackager}, + {type: 'separator'}, + ...projectTemplateType !== 'Exponent' && [ + {label: 'Build Native Modules', accelerator: 'Command+B', click: buildProject}, + {label: 'Clean', accelerator: 'CommandOrCtrl+Alt+K', click: cleanProject}, + ], + {type: 'separator'}, + {label: 'Run/Reload Simulator', accelerator: 'CmdOrCtrl+R', click: reloadSimulator}, + ...global.__DEV__ && [ + {type: 'separator'}, + {label: 'Reload Last Save', accelerator: 'CmdOrCtrl+Shift+R', click: reloadApplicationUI}, + ], + ], } this.viewMenu = { diff --git a/web/src/scripts/actions/fileActions.js b/web/src/scripts/actions/fileActions.js index 5aa2a31..631b8ac 100644 --- a/web/src/scripts/actions/fileActions.js +++ b/web/src/scripts/actions/fileActions.js @@ -17,6 +17,7 @@ import request from '../ipc/Request' import { fileTreeController } from '../filetree' +import * as ProjectTemplateUtils from '../utils/ProjectTemplateUtils' import FileConstants from 'shared/constants/ipc/FileConstants' import ProjectConstants from 'shared/constants/ipc/ProjectConstants' @@ -44,17 +45,26 @@ export const _setTopDir = (rootPath) => { } } +export const SET_PROJECT_TEMPLATE_TYPE = 'SET_PROJECT_TEMPLATE_TYPE' +export const setProjectTemplateType = (templateType) => async (dispatch, getState) => { + ProjectTemplateUtils.setApplicationMenuForTemplate(templateType) + + dispatch({type: SET_PROJECT_TEMPLATE_TYPE, payload: templateType}) +} + export const CLEAR_FILE_STATE = 'CLEAR_FILE_STATE' export const clearFileState = () => { return { type: CLEAR_FILE_STATE } } -export function setTopDir(rootPath) { - return (dispatch) => { - dispatch(_setTopDir(rootPath)) - const reset = true - fileTreeController.setRootPath(rootPath, reset) - } +export const setTopDir = (rootPath) => (dispatch) => { + dispatch(_setTopDir(rootPath)) + + const templateType = ProjectTemplateUtils.detectTemplate(rootPath) + dispatch(setProjectTemplateType(templateType)) + + const reset = true + fileTreeController.setRootPath(rootPath, reset) } const _registerPath = (filePath, info) => { diff --git a/web/src/scripts/containers/Root/Router.jsx b/web/src/scripts/containers/Root/Router.jsx index 663c926..8627b54 100644 --- a/web/src/scripts/containers/Root/Router.jsx +++ b/web/src/scripts/containers/Root/Router.jsx @@ -53,10 +53,10 @@ class AppRouter extends Component { const match = params.pathname.match(/\/workspace\/(.*)?/) if (match && match[1]) { const hexString = new Buffer(match[1], 'hex') - const path = hexString.toString('utf8') - this.props.store.dispatch(setTopDir(path)) - this.props.store.dispatch(scanLocalRegistries(path)) - this.props.store.dispatch(initializeProcessesForDir(path)) + const rootPath = hexString.toString('utf8') + this.props.store.dispatch(setTopDir(rootPath)) + this.props.store.dispatch(scanLocalRegistries(rootPath)) + this.props.store.dispatch(initializeProcessesForDir(rootPath)) } }) } diff --git a/web/src/scripts/containers/WorkspaceToolbar.jsx b/web/src/scripts/containers/WorkspaceToolbar.jsx index 8a90d94..1a3c88d 100644 --- a/web/src/scripts/containers/WorkspaceToolbar.jsx +++ b/web/src/scripts/containers/WorkspaceToolbar.jsx @@ -73,7 +73,7 @@ const stylesCreator = (theme) => { leftSection: { ...section, justifyContent: 'flex-start', - minWidth: 150, + minWidth: 255, }, centerSection: { ...section, @@ -81,7 +81,7 @@ const stylesCreator = (theme) => { rightSection: { ...section, justifyContent: 'flex-end', - minWidth: 150 + STOPLIGHT_BUTTONS_WIDTH, + minWidth: 255 + STOPLIGHT_BUTTONS_WIDTH, }, buttonGroupSeparator: { width: 7, @@ -98,14 +98,26 @@ class WorkspaceToolbar extends Component { shell.openExternal("https://decoslack.slack.com/messages/deco/") } - openDocs = () => { + openDecoDocs = () => { shell.openExternal("https://www.decosoftware.com/docs") } + openReactNativeDocs = () => { + shell.openExternal("https://facebook.github.io/react-native/docs/getting-started.html") + } + openCreateDiscussAccount = () => { shell.openExternal("https://decoslackin.herokuapp.com/") } + openExponentDocs = () => { + shell.openExternal("https://docs.getexponent.com/versions/v11.0.0/sdk/index.html#exponent-sdk") + } + + openExponentSlack = () => { + shell.openExternal("https://exponentjs.slack.com/") + } + launchSimulatorOfType = (simInfo, platform) => { if (this.props.packagerIsOff) { this.props.dispatch(runPackager()) @@ -144,18 +156,13 @@ class WorkspaceToolbar extends Component { ) } - renderDropdownMenu = () => { - const options = [ - { text: 'Open Deco Slack', action: this.openDiscuss }, - { text: 'Create Slack Account', action: this.openCreateDiscussAccount }, - ] - + renderDropdownMenu = (options) => { return (
- {_.map(options, ({text, action}, i) => ( + {_.map(options, ({text, action}, i, list) => (
@@ -170,6 +177,10 @@ class WorkspaceToolbar extends Component { setDiscussMenuVisibility = (visible) => this.setState({discussMenuOpen: visible}) + setExponentMenuVisibility = (visible) => this.setState({exponentMenuOpen: visible}) + + setDocsMenuVisibility = (visible) => this.setState({docsMenuOpen: visible}) + reloadSimulator = () => this.props.dispatch(hardReloadSimulator()) setSimulatorMenuVisibility = (visible) => this.setState({simulatorMenuOpen: visible}) @@ -194,15 +205,41 @@ class WorkspaceToolbar extends Component { } renderLeftSection() { - const {styles} = this.props + const {styles, projectTemplateType} = this.props + + const isExponentProject = projectTemplateType === 'Exponent' + + const docsOptions = [ + {text: 'Deco Docs', action: this.openDecoDocs}, + {text: 'React Native Docs', action: this.openReactNativeDocs}, + ...isExponentProject && [ + {text: 'Exponent Docs', action: this.openExponentDocs} + ], + ] + + const decoSlackOptions = [ + {text: 'Open Deco Slack', action: this.openDiscuss}, + {text: 'Create Slack Account', action: this.openCreateDiscussAccount}, + ] + + const exponentOptions = [ + {text: 'Open Exponent Slack', action: this.openExponentSlack}, + ] return (
- + + +
@@ -210,7 +247,7 @@ class WorkspaceToolbar extends Component { menuType={'platform'} offset={dropdownMenuOffset} onVisibilityChange={this.setDiscussMenuVisibility} - renderContent={this.renderDropdownMenu} + renderContent={this.renderDropdownMenu.bind(this, decoSlackOptions)} > +
+ {isExponentProject && ( + + + + + + )}
) } @@ -308,7 +361,8 @@ const mapStateToProps = (state) => { availableSimulatorsIOS: state.application.availableSimulatorsIOS, availableSimulatorsAndroid: state.application.availableSimulatorsAndroid, useGenymotion: state.preferences[CATEGORIES.GENERAL][PREFERENCES.GENERAL.USE_GENYMOTION], - publishingFeature: state.preferences[CATEGORIES.GENERAL][PREFERENCES.GENERAL.PUBLISHING_FEATURE] + publishingFeature: state.preferences[CATEGORIES.GENERAL][PREFERENCES.GENERAL.PUBLISHING_FEATURE], + projectTemplateType: state.directory.projectTemplateType, } } diff --git a/web/src/scripts/reducers/fileReducer.js b/web/src/scripts/reducers/fileReducer.js index 4efe04a..654ed7c 100644 --- a/web/src/scripts/reducers/fileReducer.js +++ b/web/src/scripts/reducers/fileReducer.js @@ -25,6 +25,7 @@ import { CLEAR_FILE_STATE, UPDATE_FILE_TREE_VERSION, SET_TOP_DIR, + SET_PROJECT_TEMPLATE_TYPE, } from '../actions/fileActions' const initialState = { @@ -77,6 +78,8 @@ const fileReducer = (state = initialState, action) => { case SET_TOP_DIR: const {rootPath, rootName} = payload return {...state, rootPath, rootName} + case SET_PROJECT_TEMPLATE_TYPE: + return {...state, projectTemplateType: payload} default: return state } diff --git a/web/src/scripts/utils/ProjectTemplateUtils.js b/web/src/scripts/utils/ProjectTemplateUtils.js new file mode 100644 index 0000000..792df33 --- /dev/null +++ b/web/src/scripts/utils/ProjectTemplateUtils.js @@ -0,0 +1,21 @@ +import path from 'path' +const fs = Electron.remote.require('fs') +const menuHandler = Electron.remote.require('./menu/menuHandler') + +export const setApplicationMenuForTemplate = (projectTemplateType) => { + menuHandler.instantiateTemplate({projectTemplateType}) +} + +export const detectTemplate = (rootPath) => { + const exponentJSON = path.resolve(rootPath, 'exp.json') + + try { + fs.statSync(exponentJSON) + + return 'Exponent' + } catch (e) { + ; + } + + return 'React Native' +} From 21c84036c058a7106bd06f08cf899e1959d737f8 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Wed, 23 Nov 2016 09:32:06 -0800 Subject: [PATCH 03/17] Enable experimental publishing --- web/src/scripts/components/preferences/GeneralPreferences.jsx | 4 ++-- web/src/scripts/components/publishing/PublishingBrowser.jsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/scripts/components/preferences/GeneralPreferences.jsx b/web/src/scripts/components/preferences/GeneralPreferences.jsx index e664d63..aeaa502 100644 --- a/web/src/scripts/components/preferences/GeneralPreferences.jsx +++ b/web/src/scripts/components/preferences/GeneralPreferences.jsx @@ -70,7 +70,7 @@ export default ({onPreferenceChange, setSystemLocationPreference, decoTheme, and placeholder={METADATA[CATEGORIES.GENERAL][PREFERENCES[CATEGORIES.GENERAL].GENYMOTION_APP].defaultValue} /> - {/* @@ -79,7 +79,7 @@ export default ({onPreferenceChange, setSystemLocationPreference, decoTheme, and type={'platform'} onChange={onPreferenceChange.bind(null, PREFERENCES.GENERAL.PUBLISHING_FEATURE)} /> - */} +
) } diff --git a/web/src/scripts/components/publishing/PublishingBrowser.jsx b/web/src/scripts/components/publishing/PublishingBrowser.jsx index e94b97c..0ed3983 100644 --- a/web/src/scripts/components/publishing/PublishingBrowser.jsx +++ b/web/src/scripts/components/publishing/PublishingBrowser.jsx @@ -124,6 +124,7 @@ export default class PublishingBrowser extends Component { return (
+ {this.renderHeader()} Date: Wed, 23 Nov 2016 11:48:41 -0800 Subject: [PATCH 04/17] Update project creation flow. File input. Input focus state. --- web/src/scripts/components/buttons/Button.jsx | 78 +++++++++ .../scripts/components/input/StringInput.jsx | 37 +++- .../components/inspector/PropertyDivider.jsx | 22 ++- .../components/inspector/PropertyField.jsx | 10 +- .../inspector/PropertyFileInput.jsx | 122 +++++++++++++ .../inspector/PropertyStringInput.jsx | 31 +++- .../components/pages/ProjectCreationPage.jsx | 164 ++++++++++++++++++ .../components/pages/TemplatesPage.jsx | 82 +-------- web/src/scripts/containers/Landing.jsx | 125 ++++++++++--- 9 files changed, 563 insertions(+), 108 deletions(-) create mode 100644 web/src/scripts/components/buttons/Button.jsx create mode 100644 web/src/scripts/components/inspector/PropertyFileInput.jsx create mode 100644 web/src/scripts/components/pages/ProjectCreationPage.jsx diff --git a/web/src/scripts/components/buttons/Button.jsx b/web/src/scripts/components/buttons/Button.jsx new file mode 100644 index 0000000..1bec157 --- /dev/null +++ b/web/src/scripts/components/buttons/Button.jsx @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import React, { Component } from 'react' +import { StylesEnhancer } from 'react-styles-provider' +import pureRender from 'pure-render-decorator' + +import SimpleButton from '../buttons/SimpleButton' + +const stylesCreator = () => { + const styles = { + normal: { + display: 'flex', + justifyContent: 'center', + color: "rgb(58,58,58)", + backgroundColor: "#ffffff", + border: '1px solid ' + "rgba(163,163,163,0.52)", + borderRadius: '3px', + textDecoration: 'none', + padding: '0 8px', + height: '20px', + fontSize: 11, + fontFamily: "'Helvetica Neue', Helvetica, sans-serif", + cursor: 'default', + flex: '0 0 75px', + fontWeight: '400', + }, + inner: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + } + + styles.active = {...styles.normal, backgroundColor: "rgba(234,233,234,0.5)"} + styles.hover = {...styles.normal, backgroundColor: "rgba(234,233,234, 1)"} + + return styles +} + +@StylesEnhancer(stylesCreator) +@pureRender +export default class PropertyStringInput extends Component { + + static defaultProps = { + children: null, + } + + render() { + const {styles, children, onClick} = this.props + + return ( + + {children} + + ) + } +} diff --git a/web/src/scripts/components/input/StringInput.jsx b/web/src/scripts/components/input/StringInput.jsx index 1756911..be8e832 100644 --- a/web/src/scripts/components/input/StringInput.jsx +++ b/web/src/scripts/components/input/StringInput.jsx @@ -22,7 +22,8 @@ import pureRender from 'pure-render-decorator' const stylesCreator = ({input}, {type, width, disabled}) => ({ input: { - ...(type === 'platform' ? input.platform : input.regular), + ...type === 'platform' ? input.platform : input.regular, + ...type !== 'platform' && {outline: 'none'}, display: 'flex', flex: '1 0 0px', width: width ? width : 0, @@ -37,6 +38,8 @@ export default class StringInput extends Component { static propTypes = { onChange: React.PropTypes.func.isRequired, onSubmit: React.PropTypes.func, + onFocus: React.PropTypes.func, + onBlur: React.PropTypes.func, value: React.PropTypes.string.isRequired, placeholder: React.PropTypes.string, width: React.PropTypes.oneOfType([ @@ -44,20 +47,46 @@ export default class StringInput extends Component { React.PropTypes.number, ]), disabled: React.PropTypes.bool, + autoFocus: React.PropTypes.bool, } static defaultProps = { className: '', style: {}, onSubmit: () => {}, + onFocus: () => {}, + onBlur: () => {}, disabled: false, + autoFocus: false, } state = {} + componentDidMount() { + const {autoFocus, value} = this.props + const {input} = this.refs + + if (autoFocus) { + console.log('string input mounted', value) + + if (value.length) { + this.setState({ + selection: {start: 0, end: value.length}, + }) + } + + input.focus() + } + } + onInputChange = (e) => this.props.onChange(e.target.value) - onBlur = () => this.setState({selection: null}) + onBlur = () => { + const {onBlur} = this.props + + this.setState({selection: null}) + onBlur() + } onKeyDown = (e) => { const {value} = e.target @@ -78,6 +107,7 @@ export default class StringInput extends Component { return break default: + this.setState({selection: null}) return break } @@ -104,7 +134,7 @@ export default class StringInput extends Component { } render() { - const {styles, value, placeholder, width, disabled} = this.props + const {styles, value, placeholder, width, disabled, onFocus} = this.props return ( ) } diff --git a/web/src/scripts/components/inspector/PropertyDivider.jsx b/web/src/scripts/components/inspector/PropertyDivider.jsx index a7baf69..1764d40 100644 --- a/web/src/scripts/components/inspector/PropertyDivider.jsx +++ b/web/src/scripts/components/inspector/PropertyDivider.jsx @@ -19,15 +19,21 @@ import React, { Component } from 'react' import { StylesEnhancer } from 'react-styles-provider' import pureRender from 'pure-render-decorator' -const stylesCreator = ({colors}, {type}) => ({ - divider: { - flex: '1 1 auto', - height: 2, - backgroundColor: type === 'vibrant' ? colors.dividerVibrant : colors.divider, - }, -}) +const stylesCreator = ({colors}, {type, active}) => { + return { + divider: { + flex: '1 1 auto', + height: 2, + backgroundColor: active + ? colors.tabs.highlight + : type === 'vibrant' + ? colors.dividerVibrant + : colors.divider, + }, + } +} -@StylesEnhancer(stylesCreator, ({type}) => ({type})) +@StylesEnhancer(stylesCreator, ({type, active}) => ({type, active})) @pureRender export default class PropertyDivider extends Component { render() { diff --git a/web/src/scripts/components/inspector/PropertyField.jsx b/web/src/scripts/components/inspector/PropertyField.jsx index 2a9b466..d924345 100644 --- a/web/src/scripts/components/inspector/PropertyField.jsx +++ b/web/src/scripts/components/inspector/PropertyField.jsx @@ -50,13 +50,19 @@ export default class PropertyField extends Component { static defaultProps = { title: '', dividerType: 'regular', + active: false, } renderDivider() { - const {dividerType} = this.props + const {dividerType, active} = this.props if (dividerType !== 'none') { - return + return ( + + ) } else { return null } diff --git a/web/src/scripts/components/inspector/PropertyFileInput.jsx b/web/src/scripts/components/inspector/PropertyFileInput.jsx new file mode 100644 index 0000000..0d18741 --- /dev/null +++ b/web/src/scripts/components/inspector/PropertyFileInput.jsx @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import React, { Component } from 'react' +import { StylesEnhancer } from 'react-styles-provider' +import pureRender from 'pure-render-decorator' +const remote = Electron.remote +const { dialog } = remote + +import PropertyField from './PropertyField' +import PropertyDivider from './PropertyDivider' +import StringInput from '../input/StringInput' +import Button from '../buttons/Button' + +const stylesCreator = ({fonts}) => ({ + container : { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + display: 'flex', + }, + row: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + display: 'flex', + height: 30, + }, +}) + +@StylesEnhancer(stylesCreator) +@pureRender +export default class PropertyStringInput extends Component { + + static defaultProps = { + title: '', + value: '', + button: 'Browse...', + onFocus: () => {}, + onBlur: () => {}, + } + + state = {focused: false} + + onFocus = () => { + const {onFocus} = this.props + const {focused} = this.state + + if (focused) return + + this.setState({focused: true}) + onFocus() + } + + onBlur = () => { + const {onBlur} = this.props + const {focused} = this.state + + if (!focused) return + + this.setState({focused: false}) + onBlur() + } + + onSelectFile = () => { + const {onChange} = this.props + + const result = dialog.showOpenDialog(remote.getCurrentWindow(), { + title: 'Select Project Location', + properties: ['openDirectory', 'createDirectory'] + }) + + if (result) { + onChange(result[0]) + } + } + + render() { + const {styles, title, value, button, onChange, actions, dividerType, disabled, autoFocus} = this.props + const {focused} = this.state + + return ( + +
+
+ +
+ +
+
+ ) + } +} diff --git a/web/src/scripts/components/inspector/PropertyStringInput.jsx b/web/src/scripts/components/inspector/PropertyStringInput.jsx index a235fc6..9097984 100644 --- a/web/src/scripts/components/inspector/PropertyStringInput.jsx +++ b/web/src/scripts/components/inspector/PropertyStringInput.jsx @@ -40,16 +40,42 @@ export default class PropertyStringInput extends Component { static defaultProps = { title: '', value: '', + onFocus: () => {}, + onBlur: () => {}, + } + + state = {focused: false} + + onFocus = () => { + const {onFocus} = this.props + const {focused} = this.state + + if (focused) return + + this.setState({focused: true}) + onFocus() + } + + onBlur = () => { + const {onBlur} = this.props + const {focused} = this.state + + if (!focused) return + + this.setState({focused: false}) + onBlur() } render() { - const {styles, title, value, onChange, actions, dividerType, disabled} = this.props + const {styles, title, value, onChange, actions, dividerType, disabled, autoFocus} = this.props + const {focused} = this.state return (
diff --git a/web/src/scripts/components/pages/ProjectCreationPage.jsx b/web/src/scripts/components/pages/ProjectCreationPage.jsx new file mode 100644 index 0000000..2be35f5 --- /dev/null +++ b/web/src/scripts/components/pages/ProjectCreationPage.jsx @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import React, { Component, PropTypes } from 'react' +import path from 'path' + +import DecoLogo from '../display/DecoLogo' +import LandingButton from '../buttons/LandingButton' +import ProjectListItem from '../buttons/ProjectListItem' +import NewIcon from '../display/NewIcon' +import PropertyStringInput from '../inspector/PropertyStringInput' +import PropertyFileInput from '../inspector/PropertyFileInput' + +const styles = { + container: { + position: 'absolute', + width: '100%', + height: '100%', + backgroundColor: "#ffffff", + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + }, + top: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + minHeight: 0, + minWidth: 0, + }, + bottom: { + display: 'flex', + flex: '0 0 auto', + backgroundColor: 'rgb(250,250,250)', + borderTop: '1px solid #E7E7E7', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: '20px', + }, + paneContainer: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'row', + alignItems: 'stretch', + minHeight: 0, + minWidth: 0, + borderTop: '1px solid #E7E7E7', + }, + categoriesPane: { + flex: '0 0 auto', + }, + logoContainer: { + flex: '0 0 auto', + padding: '35px 0px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + WebkitAppRegion: 'drag', + }, + detailsPane: { + flex: '1 1 auto', + paddingTop: 30, + paddingRight: 30, + }, + template: { + padding: 30, + }, + templateTitle: { + paddingBottom: 10, + fontSize: 14, + fontWeight: 300, + }, + templateImage: { + width: 150, + height: 150, + backgroundColor: 'rgba(0,0,0,0.05)', + boxShadow: '0 2px 4px rgba(0,0,0,0.3)', + backgroundSize: 'cover', + }, + propertySpacer: { + marginBottom: 30, + }, +} + +export default class ProjectCreationPage extends Component { + + renderTemplate = ({name, iconUrl}) => { + return ( +
+
+ {name} +
+
+
+ ) + } + + render() { + const { + onBack, + template, + onProjectNameChange, + onProjectDirectoryChange, + onCreateProject, + projectName, + projectDirectory, + } = this.props + + return ( +
+
+
+ +
+
+
+ {this.renderTemplate(template)} +
+
+ +
+ +
+
+
+
+ + Back + + + + Create Project + +
+
+ ) + } +} diff --git a/web/src/scripts/components/pages/TemplatesPage.jsx b/web/src/scripts/components/pages/TemplatesPage.jsx index df1149c..ce70c44 100644 --- a/web/src/scripts/components/pages/TemplatesPage.jsx +++ b/web/src/scripts/components/pages/TemplatesPage.jsx @@ -21,33 +21,6 @@ import DecoLogo from '../display/DecoLogo' import LandingButton from '../buttons/LandingButton' import ProjectListItem from '../buttons/ProjectListItem' -const reactNative = [ - { - "id": "blank", - "name": "Blank", - "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", - "version": "0.36.0", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" - } -] - -const exponent = [ - { - "id": "blank", - "name": "Blank", - "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", - "version": "1.7.4", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" - }, - { - "id": "tabs", - "name": "Tab Navigation", - "description": "The Tab Navigation project template includes several example screens.", - "version": "1.7.4", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_tabs.png" - } -] - const styles = { container: { position: 'absolute', @@ -142,40 +115,16 @@ styles.templateActive = { color: 'white', } -const CATEGORIES = [ - 'React Native', - 'Exponent', -] - -const TEMPLATES_FOR_CATEGORY = { - 'React Native': reactNative, - 'Exponent': exponent, -} - export default class TemplatesPage extends Component { - state = { - selectedCategory: CATEGORIES[0], - selectedTemplateIndex: null, - } - - onSelectCategory = (title) => this.setState({ - selectedCategory: title, - selectedTemplateIndex: null, - }) - - onSelectTemplate = (index) => this.setState({ - selectedTemplateIndex: index, - }) - renderCategory = (title, i) => { - const {selectedCategory} = this.state + const {selectedCategory, onSelectCategory} = this.props return (
{title}
@@ -183,13 +132,13 @@ export default class TemplatesPage extends Component { } renderTemplate = ({name, iconUrl}, i) => { - const {selectedTemplateIndex} = this.state + const {selectedTemplateIndex, onSelectTemplate} = this.props return (
{name} @@ -199,21 +148,9 @@ export default class TemplatesPage extends Component { ) } - onCreateProject = () => { - const {onCreateProject} = this.props - const {selectedCategory, selectedTemplateIndex} = this.state - - // Make sure we've picked a template - if (selectedTemplateIndex === null) return - - const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] - - onCreateProject(selectedCategory, template) - } - render() { - const {onBack} = this.props - const {selectedCategory} = this.state + const {selectedCategory, onBack, categories, templates} = this.props + const {} = this.props return (
@@ -223,10 +160,10 @@ export default class TemplatesPage extends Component {
- {CATEGORIES.map(this.renderCategory)} + {categories.map(this.renderCategory)}
- {TEMPLATES_FOR_CATEGORY[selectedCategory].map(this.renderTemplate)} + {templates.map(this.renderTemplate)}
@@ -234,9 +171,6 @@ export default class TemplatesPage extends Component { Back - - Create Project -
) diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index c97dfae..cc7213c 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -18,19 +18,65 @@ import React, { Component, PropTypes } from 'react' import { routeActions, } from 'react-router-redux' import { connect } from 'react-redux' +import { StylesProvider } from 'react-styles-provider' +const { app } = Electron.remote +import * as themes from '../themes' import { createProject, openProject, } from '../actions/applicationActions' import { resizeWindow, } from '../actions/uiActions' import RecentProjectUtils from '../utils/RecentProjectUtils' import LandingPage from '../components/pages/LandingPage' import TemplatesPage from '../components/pages/TemplatesPage' +import ProjectCreationPage from '../components/pages/ProjectCreationPage' + +const reactNative = [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "0.36.0", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" + } +] + +const exponent = [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" + }, + { + "id": "tabs", + "name": "Tab Navigation", + "description": "The Tab Navigation project template includes several example screens.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_tabs.png" + } +] + +const CATEGORIES = [ + 'React Native', + 'Exponent', +] + +const TEMPLATES_FOR_CATEGORY = { + 'React Native': reactNative, + 'Exponent': exponent, +} class Landing extends Component { state = { recentProjects: RecentProjectUtils.getProjectPaths(), page: 'landing', + template: null, + selectedCategory: CATEGORIES[0], + selectedTemplateIndex: null, + projectName: 'AwesomeProject', + projectDirectory: app.getPath('home'), } componentWillMount() { @@ -41,26 +87,58 @@ class Landing extends Component { })) } + onSelectCategory = (selectedCategory) => this.setState({selectedCategory}) + onViewLanding = () => this.setState({page: 'landing'}) onViewTemplates = () => this.setState({page: 'templates'}) - onCreateProject = (category, template) => { - const {dispatch} = this.props + onProjectNameChange = (projectName) => this.setState({projectName}) - console.log('Creating project', category, template) + onProjectDirectoryChange = (projectDirectory) => this.setState({projectDirectory}) - if (category === 'React Native') { - dispatch(createProject()) - } else { - // ... exponent stuff ... - } + onSelectTemplate = (selectedTemplateIndex) => { + this.setState({ + page: 'projectCreation', + selectedTemplateIndex, + }) + } + + onCreateProject = () => { + const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state + const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] + + console.log('create project', projectName, projectDirectory, template) + + // TODO Actually create project + } + + renderProjectCreationPage = () => { + const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state + + return ( + + ) } renderTemplatesPage = () => { + const {selectedCategory} = this.state + return ( ) @@ -72,26 +150,33 @@ class Landing extends Component { return ( { - this.props.dispatch(openProject(path)) - }} - onCreateNew={() => { - this.props.dispatch(createProject()) - }} + onOpen={(path) => this.props.dispatch(openProject(path))} + onCreateNew={() => this.props.dispatch(createProject())} onViewTemplates={this.onViewTemplates} /> ) } - render() { + renderPage = () => { const {page} = this.state - if (page === 'templates') { - return this.renderTemplatesPage() - } else { - return this.renderLandingPage() + switch (page) { + case 'templates': + return this.renderTemplatesPage() + case 'projectCreation': + return this.renderProjectCreationPage() + default: + return this.renderLandingPage() } } + + render() { + return ( + + {this.renderPage()} + + ) + } } export default connect()(Landing) From f54567ebc2fd50119f65a7bf3849c631abab9fa6 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Wed, 23 Nov 2016 16:27:56 -0800 Subject: [PATCH 05/17] Fix android location nonsense --- desktop/src/handlers/windowHandler.js | 1 - web/src/scripts/components/preferences/GeneralPreferences.jsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desktop/src/handlers/windowHandler.js b/desktop/src/handlers/windowHandler.js index 6c3633d..2c0ca26 100644 --- a/desktop/src/handlers/windowHandler.js +++ b/desktop/src/handlers/windowHandler.js @@ -57,7 +57,6 @@ class WindowHandler { bridge.on(SAVE_AS_DIALOG, this.saveAsDialog.bind(this)) bridge.on(RESIZE, this.resizeWindow.bind(this)) bridge.on(OPEN_PATH_CHOOSER_DIALOG, this.openPathChooserDialog.bind(this)) - bridge.on(OPEN_PATH_CHOOSER_DIALOG, this.openPathChooserDialog.bind(this)) bridge.on(CONFIRM_DELETE_DIALOG, this.showDeleteDialog.bind(this)) } diff --git a/web/src/scripts/components/preferences/GeneralPreferences.jsx b/web/src/scripts/components/preferences/GeneralPreferences.jsx index aeaa502..8fa92bb 100644 --- a/web/src/scripts/components/preferences/GeneralPreferences.jsx +++ b/web/src/scripts/components/preferences/GeneralPreferences.jsx @@ -56,7 +56,7 @@ export default ({onPreferenceChange, setSystemLocationPreference, decoTheme, and > @@ -66,7 +66,7 @@ export default ({onPreferenceChange, setSystemLocationPreference, decoTheme, and > From a2c095489b25d843d8f2d0aa6212865f760c7007 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Wed, 23 Nov 2016 17:47:20 -0800 Subject: [PATCH 06/17] Enforced proper project naming schemes --- web/src/scripts/containers/Landing.jsx | 37 ++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index cc7213c..6c9948f 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -75,7 +75,7 @@ class Landing extends Component { template: null, selectedCategory: CATEGORIES[0], selectedTemplateIndex: null, - projectName: 'AwesomeProject', + projectName: 'Project', projectDirectory: app.getPath('home'), } @@ -87,20 +87,51 @@ class Landing extends Component { })) } + isValidProjectName = (projectName) => { + const {selectedCategory} = this.state + + if (projectName.length === 0) { + return false + } + + if (selectedCategory === 'Exponent') { + return !!projectName[0].match(/[a-z]/) + } + + return !!projectName[0].match(/[A-Z]/) + } + + sanitizeProjectName = (projectName) => { + const {selectedCategory} = this.state + + if (selectedCategory === 'Exponent') { + return projectName.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-') + } + + const upperFirstName = projectName.length > 0 + ? projectName[0].toUpperCase() + projectName.slice(1) + : projectName + + return upperFirstName.replace(/[^a-zA-Z0-9_-]/g, '') + } + onSelectCategory = (selectedCategory) => this.setState({selectedCategory}) onViewLanding = () => this.setState({page: 'landing'}) onViewTemplates = () => this.setState({page: 'templates'}) - onProjectNameChange = (projectName) => this.setState({projectName}) + onProjectNameChange = (projectName) => this.setState({projectName: this.sanitizeProjectName(projectName)}) onProjectDirectoryChange = (projectDirectory) => this.setState({projectDirectory}) onSelectTemplate = (selectedTemplateIndex) => { + const {projectName} = this.state + this.setState({ page: 'projectCreation', selectedTemplateIndex, + projectName: this.sanitizeProjectName(projectName), }) } @@ -108,6 +139,8 @@ class Landing extends Component { const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] + if (!this.isValidProjectName(projectName)) return + console.log('create project', projectName, projectDirectory, template) // TODO Actually create project From 4979af872ec743e7cea4145a8aab0a6bf74b1b46 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Wed, 23 Nov 2016 18:10:51 -0800 Subject: [PATCH 07/17] Loading page --- .../scripts/components/pages/LoadingPage.jsx | 95 +++++++++++++++++++ web/src/scripts/containers/Landing.jsx | 13 +++ 2 files changed, 108 insertions(+) create mode 100644 web/src/scripts/components/pages/LoadingPage.jsx diff --git a/web/src/scripts/components/pages/LoadingPage.jsx b/web/src/scripts/components/pages/LoadingPage.jsx new file mode 100644 index 0000000..2f91e5c --- /dev/null +++ b/web/src/scripts/components/pages/LoadingPage.jsx @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import React, { Component, PropTypes } from 'react' + +import DecoLogo from '../display/DecoLogo' + +const styles = { + container: { + position: 'absolute', + width: '100%', + height: '100%', + backgroundColor: "#ffffff", + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + }, + top: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + minHeight: 0, + minWidth: 0, + }, + content: { + flex: '1 1 auto', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + minHeight: 0, + minWidth: 0, + borderTop: '1px solid #E7E7E7', + }, + logoContainer: { + flex: '0 0 auto', + padding: '35px 0px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + WebkitAppRegion: 'drag', + }, + spinner: { + backgroundColor: 'rgba(0,0,0,0.75)', + WebkitMaskImage: 'url("images/loading-spinner.svg")', + WebkitMaskRepeat: 'no-repeat', + WebkitMaskSize: '24px 24px', + width: 24, + height: 24, + }, + text: { + marginTop: 10, + fontSize: 12, + lineHeight: '12px', + color: '#898989', + fontWeight: 300, + }, +} + +export default class ProjectCreationPage extends Component { + render() { + const {text} = this.props + + return ( +
+
+
+ +
+
+
+
+ {text} +
+
+
+
+ ) + } +} diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index 6c9948f..e2ecd93 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -29,6 +29,7 @@ import RecentProjectUtils from '../utils/RecentProjectUtils' import LandingPage from '../components/pages/LandingPage' import TemplatesPage from '../components/pages/TemplatesPage' import ProjectCreationPage from '../components/pages/ProjectCreationPage' +import LoadingPage from '../components/pages/LoadingPage' const reactNative = [ { @@ -144,6 +145,8 @@ class Landing extends Component { console.log('create project', projectName, projectDirectory, template) // TODO Actually create project + + this.setState({page: 'loading'}) } renderProjectCreationPage = () => { @@ -177,6 +180,14 @@ class Landing extends Component { ) } + renderLoadingPage = () => { + return ( + + ) + } + renderLandingPage = () => { const {recentProjects} = this.state @@ -198,6 +209,8 @@ class Landing extends Component { return this.renderTemplatesPage() case 'projectCreation': return this.renderProjectCreationPage() + case 'loading': + return this.renderLoadingPage() default: return this.renderLandingPage() } From 4df47a2cf037b41fdccba4fe54976021f2a08d58 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Wed, 23 Nov 2016 19:47:29 -0800 Subject: [PATCH 08/17] Moved npm module installation to web side --- desktop/src/handlers/moduleHandler.js | 82 +-------------------- desktop/src/process/npmController.js | 26 +++++-- web/src/scripts/actions/dialogActions.js | 7 +- web/src/scripts/clients/ModuleClient.js | 91 +++++++++++++++++++++--- web/src/scripts/containers/Landing.jsx | 38 ++++++++-- web/src/scripts/utils/FlowUtils.js | 8 ++- 6 files changed, 148 insertions(+), 104 deletions(-) diff --git a/desktop/src/handlers/moduleHandler.js b/desktop/src/handlers/moduleHandler.js index 1dc488f..dea773b 100644 --- a/desktop/src/handlers/moduleHandler.js +++ b/desktop/src/handlers/moduleHandler.js @@ -16,46 +16,20 @@ */ import _ from 'lodash' -import fs from 'fs' import path from 'path' -import jsonfile from 'jsonfile' import dir from 'node-dir' import Logger from '../log/logger' -import npm from '../process/npmController' import bridge from '../bridge' -import { onSuccess, onError } from '../actions/genericActions' -import { startProgressBar, updateProgressBar, endProgressBar } from '../actions/uiActions' import { foundRegistries } from '../actions/moduleActions' import ModuleConstants from 'shared/constants/ipc/ModuleConstants' class ModuleHandler { register() { - bridge.on(ModuleConstants.IMPORT_MODULE, this.importModule.bind(this)) bridge.on(ModuleConstants.SCAN_PROJECT_FOR_REGISTRIES, this.scanPathForRegistries.bind(this)) } - readPackageJSON(projectPath) { - const packagePath = path.join(projectPath, 'package.json') - return new Promise((resolve, reject) => { - try { - jsonfile.readFile(packagePath, (err, obj) => { - if (err && err.code !== 'ENOENT') { - Logger.info('Failed to read package.json') - Logger.error(err) - reject(err) - } else { - resolve(obj) - } - }) - } catch (e) { - Logger.error(e) - reject(e) - } - }) - } - /** * Return a map of {filepath => package.json contents} * @param {String} dirname Directory to scan @@ -154,60 +128,6 @@ class ModuleHandler { respond(foundRegistries(registryMap)) }) } - - importModule(options, respond) { - - options.version = options.version || 'latest' - - const {name, version, path: installPath } = options - - this.readPackageJSON(options.path).then((packageJSON = {}) => { - const {dependencies} = packageJSON - - // If the dependency exists, and the version is compatible - if (dependencies && dependencies[name] && - (version === '*' || version === dependencies[name])) { - Logger.info(`npm: dependency ${name}@${version} already installed`) - respond(onSuccess(ModuleConstants.IMPORT_MODULE)) - } else { - const progressCallback = _.throttle((percent) => { - bridge.send(updateProgressBar(name, percent * 100)) - }, 250) - - bridge.send(startProgressBar(name, 0)) - - try { - const command = [ - 'install', '-S', `${name}@${version}`, - ...options.registry && ['--registry', options.registry] - ] - - Logger.info(`npm ${command.join(' ')}`) - - npm.run(command, {cwd: installPath}, (err) => { - - // Ensure a trailing throttled call doesn't fire - progressCallback.cancel() - - bridge.send(endProgressBar(name, 100)) - - if (err) { - Logger.info(`npm: dependency ${name}@${version} failed to install`) - respond(onError(ModuleConstants.IMPORT_MODULE)) - } else { - Logger.info(`npm: dependency ${name}@${version} installed successfully`) - respond(onSuccess(ModuleConstants.IMPORT_MODULE)) - } - }, progressCallback) - } catch(e) { - Logger.error(e) - respond(onError(ModuleConstants.IMPORT_MODULE)) - } - } - }) - - } - } -export default new ModuleHandler() +module.exports = new ModuleHandler() diff --git a/desktop/src/process/npmController.js b/desktop/src/process/npmController.js index e024f0e..0a583a2 100644 --- a/desktop/src/process/npmController.js +++ b/desktop/src/process/npmController.js @@ -20,12 +20,12 @@ import { fork } from 'child_process' import path from 'path' class npm { - static run(cmd = [], opts = {}, cb, progress) { + static spawn(cmd = [], opts = {}, cb, progress) { cb = once(cb) - var execPath = path.join(__dirname, '../node_modules/npm/bin/npm-cli.js') + const execPath = path.join(__dirname, '../node_modules/npm/bin/npm-cli.js') - var child = fork(execPath, cmd, opts) + const child = fork(execPath, cmd, opts) child.on('error', cb) @@ -43,6 +43,24 @@ class npm { return child } + + static run(cmd, opts, progress) { + return new Promise((resolve, reject) => { + const done = (err, code) => { + if (err) { + reject({type: 'failed', error: err}) + } else { + resolve(code) + } + } + + try { + npm.spawn(cmd, opts, done, progress) + } catch (e) { + reject({type: 'crashed', error: e}) + } + }) + } } -export default npm +module.exports = npm diff --git a/web/src/scripts/actions/dialogActions.js b/web/src/scripts/actions/dialogActions.js index e5d0fb1..90f95b6 100644 --- a/web/src/scripts/actions/dialogActions.js +++ b/web/src/scripts/actions/dialogActions.js @@ -35,7 +35,12 @@ export const openInstallModuleDialog = () => (dispatch, getState) => { onTextDone={(name) => { const state = getState() const registry = state.preferences[CATEGORIES.EDITOR][PREFERENCES.EDITOR.NPM_REGISTRY] - importModule(name, 'latest', getRootPath(state), registry) + importModule({ + name, + version: 'latest', + path: getRootPath(state), + registry, + }) }} /> ) dispatch(pushModal(dialog, true)) diff --git a/web/src/scripts/clients/ModuleClient.js b/web/src/scripts/clients/ModuleClient.js index 3d27fbf..14116b6 100644 --- a/web/src/scripts/clients/ModuleClient.js +++ b/web/src/scripts/clients/ModuleClient.js @@ -17,15 +17,15 @@ import _ from 'lodash' import semver from 'semver' +import path from 'path' +const moduleHandler = Electron.remote.require('./handlers/moduleHandler') +const npm = Electron.remote.require('./process/npmController') +const jsonfile = Electron.remote.require('jsonfile') -import request from '../ipc/Request' import { createJSX } from '../factories/module/TemplateFactory' import TemplateCache from '../persistence/TemplateCache' import RegistryCache from '../persistence/RegistryCache' -import {CACHE_STALE} from '../constants/CacheConstants' -import { - IMPORT_MODULE, -} from 'shared/constants/ipc/ModuleConstants' +import { CACHE_STALE } from '../constants/CacheConstants' import FetchUtils from '../utils/FetchUtils' const _importModule = (name, version, path, registry) => { @@ -38,8 +38,76 @@ const _importModule = (name, version, path, registry) => { } } -export const importModule = (name, version, path, registry) => { - return request(_importModule(name, version, path, registry)) +const readPackageJSON = (projectPath) => { + const packagePath = path.join(projectPath, 'package.json') + + return new Promise((resolve, reject) => { + try { + jsonfile.readFile(packagePath, (err, obj) => { + if (err && err.code !== 'ENOENT') { + console.log('Failed to read package.json') + console.error(err) + reject(err) + } else { + resolve(obj) + } + }) + } catch (e) { + console.error(e) + reject(e) + } + }) +} + +export const importModule = async (options, onProgress = () => {}) => { + const { + name, + version = 'latest', + path: installPath, + registry = '', + } = options + + const packageJSON = await readPackageJSON(installPath) + const {dependencies = {}} = packageJSON || {} + + // If the dependency exists, and the version is compatible + if ( + dependencies[name] && + (version === '*' || version === dependencies[name]) + ) { + console.log(`npm: dependency ${name}@${version} already installed`) + return + + // Download the dependency + } else { + const onProgressThrottled = _.throttle( + percent => onProgress({name, percent, completed: false}) + , 250) + + const command = [ + 'install', '-S', `${name}@${version}`, + ...registry && ['--registry', registry], + ] + + console.log(`npm ${command.join(' ')}`) + + onProgress({name, percent: 0, completed: false}) + + try { + await npm.run(command, {cwd: installPath}, onProgressThrottled) + } catch ({type, error}) { + const message = `npm: dependency ${name}@${version} failed to install` + console.log(message) + console.error(error) + throw new Error(message) + } + + // Ensure a trailing throttled call doesn't fire + onProgressThrottled.cancel() + onProgress({name, percent: 1, completed: true}) + + console.log(`npm: dependency ${name}@${version} installed successfully`) + } } export const fetchTemplateText = (url) => { @@ -61,7 +129,7 @@ export const fetchTemplateAndImportDependencies = (deps, textUrl, metadataUrl, p // TODO: multiple deps if (deps.length > 0) { const {name, version} = deps[0] - importModule(name, version, path, registry) + importModule({name, version, path, registry}) } return Promise.resolve({text: createJSX(mod)}) @@ -74,7 +142,12 @@ export const fetchTemplateAndImportDependencies = (deps, textUrl, metadataUrl, p const depVersion = deps[depName] // TODO: consider waiting for npm install to finish - importModule(depName, depVersion, path, registry) + importModule({ + name: depName, + version: depVersion, + path, + registry, + }) } const performFetch = () => { diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index e2ecd93..59dbb1f 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -19,12 +19,14 @@ import React, { Component, PropTypes } from 'react' import { routeActions, } from 'react-router-redux' import { connect } from 'react-redux' import { StylesProvider } from 'react-styles-provider' +import path from 'path' const { app } = Electron.remote import * as themes from '../themes' import { createProject, openProject, } from '../actions/applicationActions' -import { resizeWindow, } from '../actions/uiActions' +import { resizeWindow } from '../actions/uiActions' import RecentProjectUtils from '../utils/RecentProjectUtils' +import * as ModuleClient from '../clients/ModuleClient' import LandingPage from '../components/pages/LandingPage' import TemplatesPage from '../components/pages/TemplatesPage' @@ -78,6 +80,7 @@ class Landing extends Component { selectedTemplateIndex: null, projectName: 'Project', projectDirectory: app.getPath('home'), + loadingText: 'Loading...' } componentWillMount() { @@ -144,9 +147,32 @@ class Landing extends Component { console.log('create project', projectName, projectDirectory, template) - // TODO Actually create project - this.setState({page: 'loading'}) + this.installDependencies() + } + + installDependencies = async () => { + const {projectDirectory, projectName} = this.state + + // TODO not this + const fs = Electron.remote.require('fs') + fs.mkdirSync(path.resolve(projectDirectory, projectName)) + + await ModuleClient.importModule({ + name: 'lodash', + path: projectDirectory, + }, (({percent}) => { + this.setState({loadingText: `Installing lodash (${Math.ceil(percent * 100)})`}) + })) + + await ModuleClient.importModule({ + name: 'underscore', + path: projectDirectory, + }, (({percent}) => { + this.setState({loadingText: `Installing underscore (${Math.ceil(percent * 100)})`}) + })) + + this.setState({loadingText: `All done!?`}) } renderProjectCreationPage = () => { @@ -181,10 +207,10 @@ class Landing extends Component { } renderLoadingPage = () => { + const {loadingText} = this.state + return ( - + ) } diff --git a/web/src/scripts/utils/FlowUtils.js b/web/src/scripts/utils/FlowUtils.js index 04d3dfe..6a70268 100644 --- a/web/src/scripts/utils/FlowUtils.js +++ b/web/src/scripts/utils/FlowUtils.js @@ -24,10 +24,12 @@ import { importModule } from '../clients/ModuleClient' const FLOW_KEY = 'FLOW' export default { - installAndStartFlow(rootPath, npmRegistry) { + installAndStartFlow(path, registry) { + const name = 'flow-bin' + FlowController.getFlowConfigVersion() - .then(version => importModule('flow-bin', version, rootPath, npmRegistry)) - .catch(() => importModule('flow-bin', 'latest', rootPath, npmRegistry)) + .then(version => importModule({name, version, path, registry})) + .catch(() => importModule({name, version: 'latest', path, registry})) .then(() => FlowController.startServer()) }, shouldPromptForFlowInstallation(projectPath) { From 4a4b84415b043d20d3edb6b0e180ff61f8cb71d6 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 23 Nov 2016 16:29:03 -0800 Subject: [PATCH 09/17] Integrate with Exponent (part 1) --- .../libs/Scripts/deco-tool/configure.deco.js | 39 +++++--- .../deco-tool/exponent.configure.deco.js | 90 +++++++++++++++++++ .../deco-tool/template.configure.deco.js | 3 +- .../libs/Scripts/deco-tool/util/exp-cli.js | 29 ++++++ desktop/libs/Scripts/deco-tool/util/rn-cli.js | 3 +- desktop/package.json | 5 +- desktop/src/handlers/projectHandler.js | 80 ++++++++++++----- desktop/src/window/windowManager.js | 2 +- web/src/scripts/containers/Landing.jsx | 64 ++++++++++--- .../scripts/containers/WorkspaceToolbar.jsx | 5 ++ web/src/scripts/ipc/ipcActionEmitter.js | 15 +--- web/src/scripts/utils/selectProject.js | 19 ++++ 12 files changed, 288 insertions(+), 66 deletions(-) create mode 100644 desktop/libs/Scripts/deco-tool/exponent.configure.deco.js create mode 100755 desktop/libs/Scripts/deco-tool/util/exp-cli.js create mode 100644 web/src/scripts/utils/selectProject.js diff --git a/desktop/libs/Scripts/deco-tool/configure.deco.js b/desktop/libs/Scripts/deco-tool/configure.deco.js index a19a2f7..afdebb4 100644 --- a/desktop/libs/Scripts/deco-tool/configure.deco.js +++ b/desktop/libs/Scripts/deco-tool/configure.deco.js @@ -53,6 +53,15 @@ const checkEnvironmentOK = () => { return true } +const checkIsExponent = () => { + try { + fs.statSync(path.join(process.cwd(), 'exp.json')); + return true; + } catch(e) { + return false; + } +} + const checkGenymotionOK = () => { if (!process.env.GENYMOTION_APP) { const defaultGenymotionPath = `/Applications/Genymotion.app` @@ -133,18 +142,20 @@ DECO.on('list-ios-sim', function(args) { }) } - const targetAppPath = path.join(process.cwd(), path.dirname(PROJECT_SETTING.iosTarget)) - try { - fs.statSync(targetAppPath) - } catch (e) { - if (e.code == 'ENOENT') { - return Promise.reject({ - payload: [ - 'iOS simulator cannot launch without building your project.', - 'Please hit cmd + B or Tools > Build Native Modules to build your project.', - 'If you have a custom build outside of Deco, go to Deco > Project Settings and change the "iosTarget" to your .app file location' - ] - }) + if (!checkIsExponent()) { + const targetAppPath = path.join(process.cwd(), path.dirname(PROJECT_SETTING.iosTarget)) + try { + fs.statSync(targetAppPath) + } catch (e) { + if (e.code == 'ENOENT') { + return Promise.reject({ + payload: [ + 'iOS simulator cannot launch without building your project.', + 'Please hit cmd + B or Tools > Build Native Modules to build your project.', + 'If you have a custom build outside of Deco, go to Deco > Project Settings and change the "iosTarget" to your .app file location' + ] + }) + } } } @@ -436,3 +447,7 @@ DECO.on('init-template', function (args) { .pipe(fs.createWriteStream(path.join(process.cwd(), 'configure.deco.js'))) return Promise.resolve() }) + +if (checkIsExponent()) { + require('./exponent.configure.deco.js') +} diff --git a/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js b/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js new file mode 100644 index 0000000..8a1ff7c --- /dev/null +++ b/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +'use strict' + +const projectRoot = process.cwd() + +const child_process = require('child_process') +const path = require('path') +const DECO = require('deco-tool') +const xdl = require(`${projectRoot}/node_modules/xdl`) +const { Android, Project, Simulator } = xdl + +DECO.on('run-packager', function(args) { + return new Promise((resolve, reject) => { + let exponentPackagerPath = path.resolve( + projectRoot, + 'node_modules/@exponent/minimal-packager/cli.js' + ) + + var child = child_process.spawn(exponentPackagerPath, [], { + env: process.env, + cwd: process.cwd(), + stdio: 'inherit', + }) + + resolve({ child }) + }) +}) + +DECO.on('build-ios', function (args) { + // noop +}) + +DECO.on('build-android', function (args) { + // noop +}) + +DECO.on('sim-android', function(args) { + return openAppOnAndroid() +}) + +DECO.on('reload-android-app', function(args) { + return openAppOnAndroid() +}) + +DECO.on('sim-ios', function(args) { + return openAppOnIOS() +}) + +DECO.on('reload-ios-app', function(args) { + return openAppOnIOS() +}) + +function openAppOnAndroid() { + return new Promise((resolve, reject) => { + Android.openProjectAsync(projectRoot).then(() => { + resolve('Opened project on Android') + }).catch(e => { + reject(`Error opening project on Android: ${e.message}`) + }) + }) +} + +function openAppOnIOS() { + return new Promise((resolve, reject) => { + Project.getUrlAsync(projectRoot).then(url => { + Simulator.openUrlInSimulatorSafeAsync(url).then(() => { + resolve('Opened project in iOS simulator') + }).catch(e => { + reject(`Error opening project in iOS simulator: ${e.message}`) + }) + }).catch(e => { + reject(`Error opening project in iOS simulator: ${e.message}`) + }) + }) +} diff --git a/desktop/libs/Scripts/deco-tool/template.configure.deco.js b/desktop/libs/Scripts/deco-tool/template.configure.deco.js index 0564c0a..67cad0e 100644 --- a/desktop/libs/Scripts/deco-tool/template.configure.deco.js +++ b/desktop/libs/Scripts/deco-tool/template.configure.deco.js @@ -7,6 +7,7 @@ var path = require('path') var Deco = require('deco-tool') // These are settings from the local projects .deco/.settings JSON file +const client = Deco.setting.client const iosTarget = Deco.setting.iosTarget const iosProject = Deco.setting.iosProject const iosBuildScheme = Deco.setting.iosBuildScheme @@ -15,7 +16,7 @@ const packagerPort = Deco.setting.packagerPort /** * - * HOW TO USE THIS FILE (https://github.com/decosoftware/deco-ide/blob/master/desktop/CONFIGURE.MD) + * HOW TO USE THIS FILE (https://github.com/decosoftware/deco-ide/blob/master/desktop/CONFIGURE.md) * * Runs a registered function in an isolated NodeJS environment when that function's corresponding * command is triggered from within the Deco application or when run from shell as a 'deco-tool' command diff --git a/desktop/libs/Scripts/deco-tool/util/exp-cli.js b/desktop/libs/Scripts/deco-tool/util/exp-cli.js new file mode 100755 index 0000000..6c1e1c1 --- /dev/null +++ b/desktop/libs/Scripts/deco-tool/util/exp-cli.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +var path = require('path') +var fs = require('fs') +var cli; + +var cliPath = path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'cli.js' +) + +if (fs.existsSync(cliPath)) { + cli = require(cliPath); +} + +cli.run() diff --git a/desktop/libs/Scripts/deco-tool/util/rn-cli.js b/desktop/libs/Scripts/deco-tool/util/rn-cli.js index 6c1e1c1..2794914 100755 --- a/desktop/libs/Scripts/deco-tool/util/rn-cli.js +++ b/desktop/libs/Scripts/deco-tool/util/rn-cli.js @@ -18,7 +18,8 @@ var cli; var cliPath = path.resolve( process.cwd(), 'node_modules', - 'react-native', + '@exponent', + 'minimal-packager', 'cli.js' ) diff --git a/desktop/package.json b/desktop/package.json index be9feae..ef4317b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -43,7 +43,7 @@ "babel-core": "^6.4.0", "babel-runtime": "^6.11.6", "deco-simulacra": "1.0.0", - "electron-prebuilt": "1.4.3", + "electron-prebuilt": "^1.4.3", "file-tree-server": "0.0.8", "file-tree-server-git": "0.0.8", "file-tree-server-transport-electron": "0.0.1", @@ -62,6 +62,7 @@ "once": "^1.3.3", "raven": "^0.10.0", "sane": "^1.3.3", - "winston": "^2.1.1" + "winston": "^2.1.1", + "xdl": "^0.25.0" } } diff --git a/desktop/src/handlers/projectHandler.js b/desktop/src/handlers/projectHandler.js index bbf8346..1d72f93 100644 --- a/desktop/src/handlers/projectHandler.js +++ b/desktop/src/handlers/projectHandler.js @@ -53,6 +53,9 @@ import PackagerController from '../process/packagerController' import findXcodeProject from '../process/utils/findXcodeProject' import Logger from '../log/logger' +import { + User, +} from 'xdl'; let unsavedMap = {} @@ -159,34 +162,65 @@ class ProjectHandler { } } - createNewProject(payload, respond) { + async createNewProject(payload, respond) { this._resetProcessState() try { - payload.path = TEMP_PROJECT_FOLDER - this._deleteProject(payload.path) - const createProj = () => { - fs.rename(TEMP_PROJECT_FOLDER_TEMPLATE, TEMP_PROJECT_FOLDER, (err) => { - if (err) { - Logger.error(err) - respond(onError(err)) - return - } - respond(onSuccess(CREATE_NEW_PROJECT)) - bridge.send(setProject(payload.path, payload.tmp)) - this._createTemplateFolder() - }) - unsavedMap = {} + // Exponent + unsavedMap = {}; + const EXPONENT_PROJECT_PATH = '/Users/brent/coding/butter-bot'; + + // SHOW LOADING INDICATOR + console.log('Loading'); + + // IS USER SIGNED IN? NO, THEN SIGN IN + let user = await User.getCurrentUserAsync(); + + if (!user) { + await User.loginAsync('deco', 'password'); + user = await User.getCurrentUserAsync(); } - try { - fs.statSync(TEMP_PROJECT_FOLDER_TEMPLATE) - } catch (e) { - this._createTemplateFolder().then(() => { - createProj() - }) - return + if (!user) { + alert('Sorry this is broken?'); } - createProj() + + console.log(JSON.stringify(user)); + + // USER IS SIGNED IN: CREATE PROJECT + // Download the project + // Unzip it to the destination + + // respond(onSuccess(CREATE_NEW_PROJECT)); + // bridge.send(setProject(EXPONENT_PROJECT_PATH, false)); + + // ORIGINAL + // payload.path = TEMP_PROJECT_FOLDER + // this._deleteProject(payload.path) + + // const createProj = () => { + // // Rename template to /.Deco/tmp/Project + // fs.rename(TEMP_PROJECT_FOLDER_TEMPLATE, TEMP_PROJECT_FOLDER, (err) => { + // if (err) { + // Logger.error(err) + // respond(onError(err)) + // return + // } + // respond(onSuccess(CREATE_NEW_PROJECT)) + // bridge.send(setProject(payload.path, payload.tmp)) + // this._createTemplateFolder() + // }) + // unsavedMap = {} + // } + + // try { + // fs.statSync(TEMP_PROJECT_FOLDER_TEMPLATE) + // } catch (e) { + // this._createTemplateFolder().then(() => { + // createProj() + // }) + // return + // } + // createProj() } catch (e) { Logger.error(e) diff --git a/desktop/src/window/windowManager.js b/desktop/src/window/windowManager.js index d71050e..d1795b7 100644 --- a/desktop/src/window/windowManager.js +++ b/desktop/src/window/windowManager.js @@ -124,7 +124,7 @@ var WindowManager = { upgradeWindow.loadURL(WindowManager.getProjectBaseURL() + '#/upgrading') var id = new Date().getTime().toString(); - global.openWindows[id] = upgradeWindow; + global.openWindows[id] = upgradeWindow; upgradeWindow.webContents.on('did-finish-load', function() { upgradeWindow.show() diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index 59dbb1f..aadc97e 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -70,6 +70,16 @@ const TEMPLATES_FOR_CATEGORY = { 'Exponent': exponent, } +import selectProject from '../utils/selectProject'; + +import { SET_PROJECT_DIR } from 'shared/constants/ipc/ProjectConstants' + +const xdl = Electron.remote.require('xdl'); +const { + User, + Exp, +} = xdl; + class Landing extends Component { state = { @@ -148,28 +158,23 @@ class Landing extends Component { console.log('create project', projectName, projectDirectory, template) this.setState({page: 'loading'}) + + if (selectedCategory === 'React Native') { + // dispatch(createProject()) + } else { + this._createExponentProject(projectName, projectDirectory, template); + } this.installDependencies() } installDependencies = async () => { const {projectDirectory, projectName} = this.state - // TODO not this - const fs = Electron.remote.require('fs') - fs.mkdirSync(path.resolve(projectDirectory, projectName)) - - await ModuleClient.importModule({ - name: 'lodash', - path: projectDirectory, - }, (({percent}) => { - this.setState({loadingText: `Installing lodash (${Math.ceil(percent * 100)})`}) - })) - await ModuleClient.importModule({ - name: 'underscore', + name: '@exponent/minimal-packager', path: projectDirectory, }, (({percent}) => { - this.setState({loadingText: `Installing underscore (${Math.ceil(percent * 100)})`}) + this.setState({loadingText: `Installing packages (${Math.ceil(percent * 100)})`}) })) this.setState({loadingText: `All done!?`}) @@ -191,6 +196,39 @@ class Landing extends Component { ) } + // Currently this only accepts template but it will accept name and/or path (name can be + // inferred from path if we just take the last part of path). + async _createExponentProject(projectName, projectDirectory, template) { + try { + // IS USER SIGNED IN? USE THEIR ACCOUNT. OTHERWISE USE OUR DUMB ACCOUNT. + // NOTE THAT THIS LOOKS IT UP IN ~/.exponent + let user = await User.getCurrentUserAsync(); + + if (!user) { + user = await User.loginAsync({ + username: 'deco', password: 'password' + }); + } + + // ACTUALLY CREATE THE PROJECT + let projectRoot = await Exp.createNewExpAsync( + template.id, // Template id + projectDirectory, // Parent directory where to place the project + {}, // Any extra fields to add to package.json + {name: projectName} // Options, currently only name is supported + ); + + // SWITCH TO THE PROJECT + selectProject({ + type: SET_PROJECT_DIR, + absolutePath: new Buffer(projectDirectory + '/'+ projectName).toString('hex'), + isTemp: false, + } , this.props.dispatch); + } catch(e) { + alert(e.message); + } + } + renderTemplatesPage = () => { const {selectedCategory} = this.state diff --git a/web/src/scripts/containers/WorkspaceToolbar.jsx b/web/src/scripts/containers/WorkspaceToolbar.jsx index 1a3c88d..5e73d3d 100644 --- a/web/src/scripts/containers/WorkspaceToolbar.jsx +++ b/web/src/scripts/containers/WorkspaceToolbar.jsx @@ -118,6 +118,10 @@ class WorkspaceToolbar extends Component { shell.openExternal("https://exponentjs.slack.com/") } + openCreateExponentSlackAccount = () => { + shell.openExternal("https://slack.getexponent.com/") + } + launchSimulatorOfType = (simInfo, platform) => { if (this.props.packagerIsOff) { this.props.dispatch(runPackager()) @@ -224,6 +228,7 @@ class WorkspaceToolbar extends Component { const exponentOptions = [ {text: 'Open Exponent Slack', action: this.openExponentSlack}, + {text: 'Create Slack Account', action: this.openCreateExponentSlackAccount}, ] return ( diff --git a/web/src/scripts/ipc/ipcActionEmitter.js b/web/src/scripts/ipc/ipcActionEmitter.js index e176da5..0a3908a 100644 --- a/web/src/scripts/ipc/ipcActionEmitter.js +++ b/web/src/scripts/ipc/ipcActionEmitter.js @@ -101,6 +101,7 @@ import { ProcessStatus } from '../constants/ProcessStatus' import { CONTENT_PANES } from '../constants/LayoutConstants' import { closeTabWindow } from '../actions/compositeFileActions' import { clearFileState, markSaved } from '../actions/fileActions' +import selectProject from '../utils/selectProject'; /** * Ties ipc listeners to actions @@ -108,19 +109,7 @@ import { clearFileState, markSaved } from '../actions/fileActions' const ipcActionEmitter = (store) => { ipc.on(SET_PROJECT_DIR, (evt, payload) => { - const rootPath = payload.absolutePath - let query = {} - if (payload.isTemp) { - query.temp = true - } - store.dispatch(clearFileState()) - store.dispatch(editorActions.clearEditorState()) - store.dispatch(tabActions.closeAllTabs()) - const state = store.getState() - store.dispatch(routeActions.push({ - pathname: `/workspace/${rootPath}`, - query: query, - })) + selectProject(payload, store.dispatch); }) ipc.on(CUSTOM_CONFIG_ERROR, (evt, payload) => { diff --git a/web/src/scripts/utils/selectProject.js b/web/src/scripts/utils/selectProject.js new file mode 100644 index 0000000..ad2dd87 --- /dev/null +++ b/web/src/scripts/utils/selectProject.js @@ -0,0 +1,19 @@ +import * as editorActions from '../actions/editorActions' +import { routeActions, } from 'react-router-redux' +import { clearFileState } from '../actions/fileActions' +import { tabActions } from '../actions' + +export default function openProject(payload, dispatch) { + const rootPath = payload.absolutePath + let query = {} + if (payload.isTemp) { + query.temp = true + } + dispatch(clearFileState()) + dispatch(editorActions.clearEditorState()) + dispatch(tabActions.closeAllTabs()) + dispatch(routeActions.push({ + pathname: `/workspace/${rootPath}`, + query: query, + })) +} From 36be27c19f160325b1d6a7448629b18d6dfc0b55 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 23 Nov 2016 19:48:02 -0800 Subject: [PATCH 10/17] Wrap text in packager console and use lighter font-weight --- web/src/scripts/components/console/PackagerConsole.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/scripts/components/console/PackagerConsole.jsx b/web/src/scripts/components/console/PackagerConsole.jsx index def16cf..729cf69 100644 --- a/web/src/scripts/components/console/PackagerConsole.jsx +++ b/web/src/scripts/components/console/PackagerConsole.jsx @@ -23,7 +23,8 @@ const style = { color: 'rgba(255,255,255,0.8)', width: '100%', flex: '1 1 auto', - overflow: 'auto', + overflowY: 'auto', + overflowX: 'hidden', paddingLeft: 9, paddingBottom: 9, margin: 0, @@ -31,6 +32,12 @@ const style = { fontSize: 12, lineHeight: '16px', fontFamily: '"Roboto Mono", monospace', + fontWeight: 300, + overflowWrap: 'break-word', + wordWrap: 'break-word', + wordBreak: 'break-all', + hyphens: 'auto', + whiteSpace: 'pre-wrap' } } From cbdaa96de45ad4053aafa474b56992a3ca59ffb0 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 23 Nov 2016 20:03:57 -0800 Subject: [PATCH 11/17] Integrate with Exponent (part 2) --- web/src/scripts/containers/Landing.jsx | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index aadc97e..6629fac 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -149,7 +149,7 @@ class Landing extends Component { }) } - onCreateProject = () => { + onCreateProject = async () => { const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] @@ -157,22 +157,29 @@ class Landing extends Component { console.log('create project', projectName, projectDirectory, template) - this.setState({page: 'loading'}) + this.setState({page: 'loading', loadingText: 'Downloading and extracting project'}) if (selectedCategory === 'React Native') { // dispatch(createProject()) } else { - this._createExponentProject(projectName, projectDirectory, template); + await this._createExponentProjectAsync(projectName, projectDirectory, template); } - this.installDependencies() + + await this.installDependenciesAsync() + + selectProject({ + type: SET_PROJECT_DIR, + absolutePath: new Buffer(projectDirectory + '/'+ projectName).toString('hex'), + isTemp: false, + } , this.props.dispatch); } - installDependencies = async () => { + installDependenciesAsync = async () => { const {projectDirectory, projectName} = this.state await ModuleClient.importModule({ name: '@exponent/minimal-packager', - path: projectDirectory, + path: projectDirectory + '/' + projectName, }, (({percent}) => { this.setState({loadingText: `Installing packages (${Math.ceil(percent * 100)})`}) })) @@ -198,7 +205,7 @@ class Landing extends Component { // Currently this only accepts template but it will accept name and/or path (name can be // inferred from path if we just take the last part of path). - async _createExponentProject(projectName, projectDirectory, template) { + async _createExponentProjectAsync(projectName, projectDirectory, template) { try { // IS USER SIGNED IN? USE THEIR ACCOUNT. OTHERWISE USE OUR DUMB ACCOUNT. // NOTE THAT THIS LOOKS IT UP IN ~/.exponent @@ -217,13 +224,6 @@ class Landing extends Component { {}, // Any extra fields to add to package.json {name: projectName} // Options, currently only name is supported ); - - // SWITCH TO THE PROJECT - selectProject({ - type: SET_PROJECT_DIR, - absolutePath: new Buffer(projectDirectory + '/'+ projectName).toString('hex'), - isTemp: false, - } , this.props.dispatch); } catch(e) { alert(e.message); } From 03d17e18e512e0f6002e50802c2b3360dfb1fe0e Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Fri, 25 Nov 2016 18:13:57 -0800 Subject: [PATCH 12/17] Easier setup, add dtrace-provider to rebuild-native-modules task, remove node-inspector --- .gitignore | 2 ++ README.md | 15 ++++----------- desktop/gulpfile.babel.js | 2 +- desktop/package.json | 1 - tools/hyperinstall.json | 5 +++++ tools/install-dependencies | 19 +++++++++++++++++++ tools/npm-hyperinstall | 11 +++++++++++ 7 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 tools/hyperinstall.json create mode 100755 tools/install-dependencies create mode 100755 tools/npm-hyperinstall diff --git a/.gitignore b/.gitignore index 6046bd1..aa30f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /app /dist /desktop/tests/integration/snapshot/tmp + +tools/.hyperinstall-state.json diff --git a/README.md b/README.md index 8ef70fc..ca9294c 100644 --- a/README.md +++ b/README.md @@ -56,18 +56,11 @@ Linux is not supported at this time. Windows is not supported at this time. ### Clone and Install Dependencies + ``` -$ git clone git@github.com:decosoftware/deco-ide -$ cd ./deco-ide/web -$ npm install -$ bundle install -$ cd ../desktop -$ npm install -$ npm run copy-libs -$ cd ../shared -$ npm install -$ cd ../desktop/libs/Scripts/sync-service -$ npm install +npm i -g hyperinstall +git clone git@github.com:decosoftware/deco-ide +./deco-ide/tools/install-dependencies ``` ### Development diff --git a/desktop/gulpfile.babel.js b/desktop/gulpfile.babel.js index 5236fbb..e56c742 100644 --- a/desktop/gulpfile.babel.js +++ b/desktop/gulpfile.babel.js @@ -82,7 +82,7 @@ gulp.task('dist', ['modify-plist'], (callback) => { }) gulp.task('rebuild-native-modules', () => { - const modules = ['git-utils', 'nodobjc', 'ffi', 'ref', 'ref-struct'] + const modules = ['git-utils', 'nodobjc', 'ffi', 'ref', 'ref-struct', 'dtrace-provider'] modules.forEach(module => { console.log('Building native module', module, 'for version', NODE_MODULES_VERSION) diff --git a/desktop/package.json b/desktop/package.json index ef4317b..52e97da 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -24,7 +24,6 @@ "image-diff": "^1.6.3", "jest": "^15.1.1", "mocha": "^3.0.2", - "node-inspector": "^0.12.8", "node-pre-gyp": "^0.6.29", "plist": "^1.2.0", "rimraf": "^2.5.4", diff --git a/tools/hyperinstall.json b/tools/hyperinstall.json new file mode 100644 index 0000000..a348eb4 --- /dev/null +++ b/tools/hyperinstall.json @@ -0,0 +1,5 @@ +{ + "../web": 0, + "../desktop": 0, + "../shared": 0 +} diff --git a/tools/install-dependencies b/tools/install-dependencies new file mode 100755 index 0000000..5e9ccb0 --- /dev/null +++ b/tools/install-dependencies @@ -0,0 +1,19 @@ +#!/bin/bash + +set -eu + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +cd $ROOT + +./npm-hyperinstall + +pushd ../web +command -v bundle >/dev/null 2>&1 || { +echo >&2 "Bundler is not in your PATH; run "gem install bundler""; +exit 1; +} +bundle install +popd + +pushd ../desktop +npm run copy-libs diff --git a/tools/npm-hyperinstall b/tools/npm-hyperinstall new file mode 100755 index 0000000..5a5dfeb --- /dev/null +++ b/tools/npm-hyperinstall @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +cd $ROOT +command -v hyperinstall >/dev/null 2>&1 || { +echo >&2 "Hyperinstall is not in your PATH; run "npm install -g hyperinstall""; +exit 1; +} +hyperinstall install $@ From 557a01a28bd5bab11d0c9697599fb60dfab92d5e Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Fri, 25 Nov 2016 18:45:15 -0800 Subject: [PATCH 13/17] Inline requires fix native module warnings --- .../Scripts/deco-tool/exponent.configure.deco.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js b/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js index 8a1ff7c..7d6a7f4 100644 --- a/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js +++ b/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js @@ -21,8 +21,6 @@ const projectRoot = process.cwd() const child_process = require('child_process') const path = require('path') const DECO = require('deco-tool') -const xdl = require(`${projectRoot}/node_modules/xdl`) -const { Android, Project, Simulator } = xdl DECO.on('run-packager', function(args) { return new Promise((resolve, reject) => { @@ -67,7 +65,9 @@ DECO.on('reload-ios-app', function(args) { function openAppOnAndroid() { return new Promise((resolve, reject) => { - Android.openProjectAsync(projectRoot).then(() => { + const xdl = require(`${projectRoot}/node_modules/xdl`) + + xdl.Android.openProjectAsync(projectRoot).then(() => { resolve('Opened project on Android') }).catch(e => { reject(`Error opening project on Android: ${e.message}`) @@ -77,8 +77,10 @@ function openAppOnAndroid() { function openAppOnIOS() { return new Promise((resolve, reject) => { - Project.getUrlAsync(projectRoot).then(url => { - Simulator.openUrlInSimulatorSafeAsync(url).then(() => { + const xdl = require(`${projectRoot}/node_modules/xdl`) + + xdl.Project.getUrlAsync(projectRoot).then(url => { + xdl.Simulator.openUrlInSimulatorSafeAsync(url).then(() => { resolve('Opened project in iOS simulator') }).catch(e => { reject(`Error opening project in iOS simulator: ${e.message}`) From 424e4ce6a92c7fb1fd7bbef8f6f2330c80a49fc7 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Fri, 2 Dec 2016 10:54:21 -0800 Subject: [PATCH 14/17] Cleanup --- .../deco-tool/template.configure.deco.js | 1 - .../libs/Scripts/deco-tool/util/exp-cli.js | 29 ------- desktop/src/handlers/projectHandler.js | 80 ++++++------------- 3 files changed, 23 insertions(+), 87 deletions(-) delete mode 100755 desktop/libs/Scripts/deco-tool/util/exp-cli.js diff --git a/desktop/libs/Scripts/deco-tool/template.configure.deco.js b/desktop/libs/Scripts/deco-tool/template.configure.deco.js index 67cad0e..742ac81 100644 --- a/desktop/libs/Scripts/deco-tool/template.configure.deco.js +++ b/desktop/libs/Scripts/deco-tool/template.configure.deco.js @@ -7,7 +7,6 @@ var path = require('path') var Deco = require('deco-tool') // These are settings from the local projects .deco/.settings JSON file -const client = Deco.setting.client const iosTarget = Deco.setting.iosTarget const iosProject = Deco.setting.iosProject const iosBuildScheme = Deco.setting.iosBuildScheme diff --git a/desktop/libs/Scripts/deco-tool/util/exp-cli.js b/desktop/libs/Scripts/deco-tool/util/exp-cli.js deleted file mode 100755 index 6c1e1c1..0000000 --- a/desktop/libs/Scripts/deco-tool/util/exp-cli.js +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env node - -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow - */ - -var path = require('path') -var fs = require('fs') -var cli; - -var cliPath = path.resolve( - process.cwd(), - 'node_modules', - 'react-native', - 'cli.js' -) - -if (fs.existsSync(cliPath)) { - cli = require(cliPath); -} - -cli.run() diff --git a/desktop/src/handlers/projectHandler.js b/desktop/src/handlers/projectHandler.js index 1d72f93..bbf8346 100644 --- a/desktop/src/handlers/projectHandler.js +++ b/desktop/src/handlers/projectHandler.js @@ -53,9 +53,6 @@ import PackagerController from '../process/packagerController' import findXcodeProject from '../process/utils/findXcodeProject' import Logger from '../log/logger' -import { - User, -} from 'xdl'; let unsavedMap = {} @@ -162,65 +159,34 @@ class ProjectHandler { } } - async createNewProject(payload, respond) { + createNewProject(payload, respond) { this._resetProcessState() try { - // Exponent - unsavedMap = {}; - const EXPONENT_PROJECT_PATH = '/Users/brent/coding/butter-bot'; - - // SHOW LOADING INDICATOR - console.log('Loading'); - - // IS USER SIGNED IN? NO, THEN SIGN IN - let user = await User.getCurrentUserAsync(); - - if (!user) { - await User.loginAsync('deco', 'password'); - user = await User.getCurrentUserAsync(); + payload.path = TEMP_PROJECT_FOLDER + this._deleteProject(payload.path) + const createProj = () => { + fs.rename(TEMP_PROJECT_FOLDER_TEMPLATE, TEMP_PROJECT_FOLDER, (err) => { + if (err) { + Logger.error(err) + respond(onError(err)) + return + } + respond(onSuccess(CREATE_NEW_PROJECT)) + bridge.send(setProject(payload.path, payload.tmp)) + this._createTemplateFolder() + }) + unsavedMap = {} } - if (!user) { - alert('Sorry this is broken?'); + try { + fs.statSync(TEMP_PROJECT_FOLDER_TEMPLATE) + } catch (e) { + this._createTemplateFolder().then(() => { + createProj() + }) + return } - - console.log(JSON.stringify(user)); - - // USER IS SIGNED IN: CREATE PROJECT - // Download the project - // Unzip it to the destination - - // respond(onSuccess(CREATE_NEW_PROJECT)); - // bridge.send(setProject(EXPONENT_PROJECT_PATH, false)); - - // ORIGINAL - // payload.path = TEMP_PROJECT_FOLDER - // this._deleteProject(payload.path) - - // const createProj = () => { - // // Rename template to /.Deco/tmp/Project - // fs.rename(TEMP_PROJECT_FOLDER_TEMPLATE, TEMP_PROJECT_FOLDER, (err) => { - // if (err) { - // Logger.error(err) - // respond(onError(err)) - // return - // } - // respond(onSuccess(CREATE_NEW_PROJECT)) - // bridge.send(setProject(payload.path, payload.tmp)) - // this._createTemplateFolder() - // }) - // unsavedMap = {} - // } - - // try { - // fs.statSync(TEMP_PROJECT_FOLDER_TEMPLATE) - // } catch (e) { - // this._createTemplateFolder().then(() => { - // createProj() - // }) - // return - // } - // createProj() + createProj() } catch (e) { Logger.error(e) From aab942248da3b9323d177295d5f61d0561b9e9b9 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Fri, 2 Dec 2016 12:15:17 -0800 Subject: [PATCH 15/17] Refactor exponent code. Disable via webpack config. --- .../scripts/components/pages/LandingPage.jsx | 19 +- .../constants/ProjectTemplateConstants.js | 36 ++++ web/src/scripts/containers/Landing.jsx | 168 ++++-------------- .../scripts/containers/WorkspaceToolbar.jsx | 1 - web/src/scripts/utils/Exponent.js | 70 ++++++++ web/src/scripts/utils/ProjectTemplateUtils.js | 31 +++- web/webpack.config.js | 3 +- web/webpack.production.config.js | 1 + 8 files changed, 183 insertions(+), 146 deletions(-) create mode 100644 web/src/scripts/constants/ProjectTemplateConstants.js create mode 100644 web/src/scripts/utils/Exponent.js diff --git a/web/src/scripts/components/pages/LandingPage.jsx b/web/src/scripts/components/pages/LandingPage.jsx index ceabb3a..50bd011 100644 --- a/web/src/scripts/components/pages/LandingPage.jsx +++ b/web/src/scripts/components/pages/LandingPage.jsx @@ -31,7 +31,6 @@ const style = { display: 'flex', flexDirection: 'column', alignItems: 'stretch', - WebkitAppRegion: 'drag', } const topStyle = { @@ -69,13 +68,14 @@ const logoWrapperStyle = { display: 'flex', justifyContent: 'center', alignItems: 'center', + WebkitAppRegion: 'drag', } const buttonDividerStyle = { height: 15, } -const LandingPage = ({ onOpen, onCreateNew, recentProjects, onViewTemplates }) => { +const LandingPage = ({ onOpen, onCreateNew, recentProjects, onViewTemplates, showTemplates }) => { return (
@@ -107,14 +107,19 @@ const LandingPage = ({ onOpen, onCreateNew, recentProjects, onViewTemplates }) =
+ onClick={onCreateNew} + > New Project -
- - Project Templates... - + {showTemplates && ( +
+ )} + {showTemplates && ( + + Project Templates... + + )}
) diff --git a/web/src/scripts/constants/ProjectTemplateConstants.js b/web/src/scripts/constants/ProjectTemplateConstants.js new file mode 100644 index 0000000..11850df --- /dev/null +++ b/web/src/scripts/constants/ProjectTemplateConstants.js @@ -0,0 +1,36 @@ + +export const CATEGORY_REACT_NATIVE = 'React Native' +export const CATEGORY_EXPONENT = 'Exponent' + +export const CATEGORIES = [ + CATEGORY_REACT_NATIVE, + CATEGORY_EXPONENT, +] + +export const TEMPLATES_FOR_CATEGORY = { + [CATEGORY_REACT_NATIVE]: [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "0.36.0", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png", + }, + ], + [CATEGORY_EXPONENT]: [ + { + "id": "blank", + "name": "Blank", + "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png", + }, + { + "id": "tabs", + "name": "Tab Navigation", + "description": "The Tab Navigation project template includes several example screens.", + "version": "1.7.4", + "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_tabs.png", + }, + ], +} diff --git a/web/src/scripts/containers/Landing.jsx b/web/src/scripts/containers/Landing.jsx index 6629fac..93e6e97 100644 --- a/web/src/scripts/containers/Landing.jsx +++ b/web/src/scripts/containers/Landing.jsx @@ -26,67 +26,24 @@ import * as themes from '../themes' import { createProject, openProject, } from '../actions/applicationActions' import { resizeWindow } from '../actions/uiActions' import RecentProjectUtils from '../utils/RecentProjectUtils' -import * as ModuleClient from '../clients/ModuleClient' - +import * as Exponent from '../utils/Exponent' +import * as ProjectTemplateUtils from '../utils/ProjectTemplateUtils' +import * as ProjectTemplateConstants from '../constants/ProjectTemplateConstants' import LandingPage from '../components/pages/LandingPage' import TemplatesPage from '../components/pages/TemplatesPage' import ProjectCreationPage from '../components/pages/ProjectCreationPage' import LoadingPage from '../components/pages/LoadingPage' -const reactNative = [ - { - "id": "blank", - "name": "Blank", - "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", - "version": "0.36.0", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" - } -] - -const exponent = [ - { - "id": "blank", - "name": "Blank", - "description": "The Blank project template includes the minimum dependencies to run and an empty root component.", - "version": "1.7.4", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_blank.png" - }, - { - "id": "tabs", - "name": "Tab Navigation", - "description": "The Tab Navigation project template includes several example screens.", - "version": "1.7.4", - "iconUrl": "https://s3.amazonaws.com/exp-starter-apps/template_icon_tabs.png" - } -] - -const CATEGORIES = [ - 'React Native', - 'Exponent', -] - -const TEMPLATES_FOR_CATEGORY = { - 'React Native': reactNative, - 'Exponent': exponent, -} - -import selectProject from '../utils/selectProject'; - +import selectProject from '../utils/selectProject' import { SET_PROJECT_DIR } from 'shared/constants/ipc/ProjectConstants' -const xdl = Electron.remote.require('xdl'); -const { - User, - Exp, -} = xdl; - class Landing extends Component { state = { recentProjects: RecentProjectUtils.getProjectPaths(), page: 'landing', template: null, - selectedCategory: CATEGORIES[0], + selectedCategory: ProjectTemplateConstants.CATEGORY_REACT_NATIVE, selectedTemplateIndex: null, projectName: 'Project', projectDirectory: app.getPath('home'), @@ -101,90 +58,56 @@ class Landing extends Component { })) } - isValidProjectName = (projectName) => { - const {selectedCategory} = this.state - - if (projectName.length === 0) { - return false - } - - if (selectedCategory === 'Exponent') { - return !!projectName[0].match(/[a-z]/) - } - - return !!projectName[0].match(/[A-Z]/) - } - - sanitizeProjectName = (projectName) => { - const {selectedCategory} = this.state - - if (selectedCategory === 'Exponent') { - return projectName.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-') - } - - const upperFirstName = projectName.length > 0 - ? projectName[0].toUpperCase() + projectName.slice(1) - : projectName - - return upperFirstName.replace(/[^a-zA-Z0-9_-]/g, '') - } - onSelectCategory = (selectedCategory) => this.setState({selectedCategory}) onViewLanding = () => this.setState({page: 'landing'}) onViewTemplates = () => this.setState({page: 'templates'}) - onProjectNameChange = (projectName) => this.setState({projectName: this.sanitizeProjectName(projectName)}) + onProjectNameChange = (projectName) => { + const {selectedCategory} = this.state + const sanitizedName = ProjectTemplateUtils.sanitizeProjectName(projectName, selectedCategory) + + this.setState({projectName: sanitizedName}) + } onProjectDirectoryChange = (projectDirectory) => this.setState({projectDirectory}) onSelectTemplate = (selectedTemplateIndex) => { - const {projectName} = this.state + const {selectedCategory, projectName} = this.state + const sanitizedName = ProjectTemplateUtils.sanitizeProjectName(projectName, selectedCategory) this.setState({ page: 'projectCreation', selectedTemplateIndex, - projectName: this.sanitizeProjectName(projectName), + projectName: sanitizedName, }) } onCreateProject = async () => { + const {dispatch} = this.props const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state - const template = TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] - - if (!this.isValidProjectName(projectName)) return + const template = ProjectTemplateConstants.TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex] - console.log('create project', projectName, projectDirectory, template) + if (!ProjectTemplateUtils.isValidProjectName(projectName, selectedCategory)) return this.setState({page: 'loading', loadingText: 'Downloading and extracting project'}) - if (selectedCategory === 'React Native') { - // dispatch(createProject()) - } else { - await this._createExponentProjectAsync(projectName, projectDirectory, template); - } + if (selectedCategory === ProjectTemplateConstants.CATEGORY_EXPONENT) { + const progressCallback = (loadingText) => this.setState({loadingText}) - await this.installDependenciesAsync() + await Exponent.createProject(projectName, projectDirectory, template, progressCallback) - selectProject({ - type: SET_PROJECT_DIR, - absolutePath: new Buffer(projectDirectory + '/'+ projectName).toString('hex'), - isTemp: false, - } , this.props.dispatch); - } - - installDependenciesAsync = async () => { - const {projectDirectory, projectName} = this.state - - await ModuleClient.importModule({ - name: '@exponent/minimal-packager', - path: projectDirectory + '/' + projectName, - }, (({percent}) => { - this.setState({loadingText: `Installing packages (${Math.ceil(percent * 100)})`}) - })) + selectProject({ + type: SET_PROJECT_DIR, + absolutePath: new Buffer(path.join(projectDirectory, projectName)).toString('hex'), + isTemp: false, + }, dispatch) + } else { - this.setState({loadingText: `All done!?`}) + // TODO actually use the selected project name & directory + dispatch(createProject()) + } } renderProjectCreationPage = () => { @@ -194,7 +117,7 @@ class Landing extends Component { { const {selectedCategory} = this.state return ( this.props.dispatch(openProject(path))} onCreateNew={() => this.props.dispatch(createProject())} onViewTemplates={this.onViewTemplates} + showTemplates={SHOW_PROJECT_TEMPLATES} /> ) } diff --git a/web/src/scripts/containers/WorkspaceToolbar.jsx b/web/src/scripts/containers/WorkspaceToolbar.jsx index 5e73d3d..e59c65c 100644 --- a/web/src/scripts/containers/WorkspaceToolbar.jsx +++ b/web/src/scripts/containers/WorkspaceToolbar.jsx @@ -201,7 +201,6 @@ class WorkspaceToolbar extends Component { this.props.dispatch(setConsoleVisibility(!consoleVisible)) } - toggleRightPane = (content) => { const {rightSidebarVisible} = this.props diff --git a/web/src/scripts/utils/Exponent.js b/web/src/scripts/utils/Exponent.js new file mode 100644 index 0000000..6ec257b --- /dev/null +++ b/web/src/scripts/utils/Exponent.js @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2015 Deco Software Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +import path from 'path' +const xdl = Electron.remote.require('xdl') +const { User, Exp } = xdl + +import * as ModuleClient from '../clients/ModuleClient' + +const createExponentProject = async (projectName, projectDirectory, template) => { + try { + // Is user signed in? Use their account + // NOTE: This looks it up in ~/.exponent + const user = await User.getCurrentUserAsync() + + // Otherwise use our dummy account + if (!user) { + await User.loginAsync({username: 'deco', password: 'password'}) + } + + await Exp.createNewExpAsync( + template.id, // Template id + projectDirectory, // Parent directory where to place the project + {}, // Any extra fields to add to package.json + {name: projectName} // Options, currently only name is supported + ) + } catch(e) { + alert(e.message) + } +} + +const installDependenciesAsync = async (projectName, projectDirectory, progressCallback) => { + await ModuleClient.importModule({ + name: '@exponent/minimal-packager', + path: path.join(projectDirectory, projectName), + }, (({percent}) => { + progressCallback(`Installing packages (${Math.ceil(percent * 100)})`) + })) + + progressCallback(`All done!`) +} + +export const createProject = async (projectName, projectDirectory, template, progressCallback) => { + await createExponentProject(projectName, projectDirectory, template) + await installDependenciesAsync(projectName, projectDirectory, progressCallback) +} + +export const sanitizeProjectName = (projectName) => { + return projectName.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-') +} + +// Assumes sanitized project name +export const isValidProjectName = (projectName) => { + return !!projectName[0].match(/[a-z]/) +} diff --git a/web/src/scripts/utils/ProjectTemplateUtils.js b/web/src/scripts/utils/ProjectTemplateUtils.js index 792df33..37d486d 100644 --- a/web/src/scripts/utils/ProjectTemplateUtils.js +++ b/web/src/scripts/utils/ProjectTemplateUtils.js @@ -2,6 +2,9 @@ import path from 'path' const fs = Electron.remote.require('fs') const menuHandler = Electron.remote.require('./menu/menuHandler') +import * as Exponent from './Exponent' +import * as ProjectTemplateConstants from '../constants/ProjectTemplateConstants' + export const setApplicationMenuForTemplate = (projectTemplateType) => { menuHandler.instantiateTemplate({projectTemplateType}) } @@ -12,10 +15,34 @@ export const detectTemplate = (rootPath) => { try { fs.statSync(exponentJSON) - return 'Exponent' + return ProjectTemplateConstants.CATEGORY_EXPONENT } catch (e) { ; } - return 'React Native' + return ProjectTemplateConstants.CATEGORY_REACT_NATIVE +} + +export const isValidProjectName = (projectName, projectTemplateType) => { + if (projectName.length === 0) { + return false + } + + if (projectTemplateType === ProjectTemplateConstants.CATEGORY_EXPONENT) { + return Exponent.isValidProjectName(projectName) + } + + return !!projectName[0].match(/[A-Z]/) +} + +export const sanitizeProjectName = (projectName, projectTemplateType) => { + if (projectTemplateType === ProjectTemplateConstants.CATEGORY_EXPONENT) { + return Exponent.sanitizeProjectName(projectName) + } + + const upperFirstName = projectName.length > 0 + ? projectName[0].toUpperCase() + projectName.slice(1) + : projectName + + return upperFirstName.replace(/[^a-zA-Z0-9_-]/g, '') } diff --git a/web/webpack.config.js b/web/webpack.config.js index a0a40f6..b5a2bc4 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -39,7 +39,8 @@ module.exports = { new webpack.NoErrorsPlugin(), new webpack.IgnorePlugin(/vertx/), // https://github.com/webpack/webpack/issues/353 new webpack.DefinePlugin({ - "SHOW_STORYBOARD": 0, + "SHOW_STORYBOARD": false, + "SHOW_PROJECT_TEMPLATES": false, "process.env": { NODE_ENV: JSON.stringify("local") } diff --git a/web/webpack.production.config.js b/web/webpack.production.config.js index 93c7cfb..7fb17e1 100644 --- a/web/webpack.production.config.js +++ b/web/webpack.production.config.js @@ -25,6 +25,7 @@ module.exports = { plugins: [ new webpack.DefinePlugin({ "SHOW_STORYBOARD": 0, + "SHOW_PROJECT_TEMPLATES": 0, // This has effect on the react lib size. "process.env": { NODE_ENV: JSON.stringify("production") From 275170be1ad68a4b6a2f345a92f35a79e56401b6 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Fri, 2 Dec 2016 12:19:50 -0800 Subject: [PATCH 16/17] Revert rn-cli --- desktop/libs/Scripts/deco-tool/util/rn-cli.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/desktop/libs/Scripts/deco-tool/util/rn-cli.js b/desktop/libs/Scripts/deco-tool/util/rn-cli.js index 2794914..6c1e1c1 100755 --- a/desktop/libs/Scripts/deco-tool/util/rn-cli.js +++ b/desktop/libs/Scripts/deco-tool/util/rn-cli.js @@ -18,8 +18,7 @@ var cli; var cliPath = path.resolve( process.cwd(), 'node_modules', - '@exponent', - 'minimal-packager', + 'react-native', 'cli.js' ) From 512ee1764b5cea943c2f94efad5c92fa2f241868 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Fri, 2 Dec 2016 12:25:16 -0800 Subject: [PATCH 17/17] Style cleanup --- web/src/scripts/components/input/StringInput.jsx | 2 -- web/src/scripts/ipc/ipcActionEmitter.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/src/scripts/components/input/StringInput.jsx b/web/src/scripts/components/input/StringInput.jsx index be8e832..bd408b6 100644 --- a/web/src/scripts/components/input/StringInput.jsx +++ b/web/src/scripts/components/input/StringInput.jsx @@ -67,8 +67,6 @@ export default class StringInput extends Component { const {input} = this.refs if (autoFocus) { - console.log('string input mounted', value) - if (value.length) { this.setState({ selection: {start: 0, end: value.length}, diff --git a/web/src/scripts/ipc/ipcActionEmitter.js b/web/src/scripts/ipc/ipcActionEmitter.js index 0a3908a..9dd5a9c 100644 --- a/web/src/scripts/ipc/ipcActionEmitter.js +++ b/web/src/scripts/ipc/ipcActionEmitter.js @@ -101,7 +101,7 @@ import { ProcessStatus } from '../constants/ProcessStatus' import { CONTENT_PANES } from '../constants/LayoutConstants' import { closeTabWindow } from '../actions/compositeFileActions' import { clearFileState, markSaved } from '../actions/fileActions' -import selectProject from '../utils/selectProject'; +import selectProject from '../utils/selectProject' /** * Ties ipc listeners to actions @@ -109,7 +109,7 @@ import selectProject from '../utils/selectProject'; const ipcActionEmitter = (store) => { ipc.on(SET_PROJECT_DIR, (evt, payload) => { - selectProject(payload, store.dispatch); + selectProject(payload, store.dispatch) }) ipc.on(CUSTOM_CONFIG_ERROR, (evt, payload) => {