diff --git a/.gitignore b/.gitignore
index 6046bd15..aa30f9da 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 8ef70fc9..ca9294c9 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 5236fbb6..e56c7421 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/libs/Scripts/deco-tool/configure.deco.js b/desktop/libs/Scripts/deco-tool/configure.deco.js
index a19a2f73..afdebb4f 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 00000000..7d6a7f4a
--- /dev/null
+++ b/desktop/libs/Scripts/deco-tool/exponent.configure.deco.js
@@ -0,0 +1,92 @@
+/**
+ * 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')
+
+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) => {
+ 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}`)
+ })
+ })
+}
+
+function openAppOnIOS() {
+ return new Promise((resolve, reject) => {
+ 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}`)
+ })
+ }).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 0564c0a0..742ac81b 100644
--- a/desktop/libs/Scripts/deco-tool/template.configure.deco.js
+++ b/desktop/libs/Scripts/deco-tool/template.configure.deco.js
@@ -15,7 +15,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/package.json b/desktop/package.json
index be9feaec..52e97da3 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",
@@ -43,7 +42,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 +61,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/moduleHandler.js b/desktop/src/handlers/moduleHandler.js
index 1dc488fd..dea773bc 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/handlers/windowHandler.js b/desktop/src/handlers/windowHandler.js
index 6c3633d4..2c0ca269 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/desktop/src/menu/menuHandler.js b/desktop/src/menu/menuHandler.js
index e866087f..7f3dfcd6 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 96331186..37bba3ba 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/desktop/src/process/npmController.js b/desktop/src/process/npmController.js
index e024f0ee..0a583a24 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/desktop/src/window/windowManager.js b/desktop/src/window/windowManager.js
index d71050e7..d1795b78 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/tools/hyperinstall.json b/tools/hyperinstall.json
new file mode 100644
index 00000000..a348eb43
--- /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 00000000..5e9ccb0b
--- /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 00000000..5a5dfebb
--- /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 $@
diff --git a/web/src/scripts/actions/dialogActions.js b/web/src/scripts/actions/dialogActions.js
index e5d0fb1e..90f95b64 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/actions/fileActions.js b/web/src/scripts/actions/fileActions.js
index 5aa2a31d..631b8ac9 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/clients/ModuleClient.js b/web/src/scripts/clients/ModuleClient.js
index 3d27fbf8..14116b6f 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/components/buttons/Button.jsx b/web/src/scripts/components/buttons/Button.jsx
new file mode 100644
index 00000000..1bec157e
--- /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/buttons/LandingButton.jsx b/web/src/scripts/components/buttons/LandingButton.jsx
index 755018d7..c556ed4a 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/console/PackagerConsole.jsx b/web/src/scripts/components/console/PackagerConsole.jsx
index def16cff..729cf690 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'
}
}
diff --git a/web/src/scripts/components/input/StringInput.jsx b/web/src/scripts/components/input/StringInput.jsx
index 17569116..bd408b62 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,44 @@ 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) {
+ 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 +105,7 @@ export default class StringInput extends Component {
return
break
default:
+ this.setState({selection: null})
return
break
}
@@ -104,7 +132,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 a7baf691..1764d404 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 2a9b466e..d924345e 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 00000000..0d18741e
--- /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 a235fc66..90979846 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/LandingPage.jsx b/web/src/scripts/components/pages/LandingPage.jsx
index 526c2923..50bd0111 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 = {
@@ -44,13 +43,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 = {
@@ -69,11 +68,16 @@ const logoWrapperStyle = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
+ WebkitAppRegion: 'drag',
+}
+
+const buttonDividerStyle = {
+ height: 15,
}
-const LandingPage = ({ onOpen, onCreateNew, recentProjects }) => {
+const LandingPage = ({ onOpen, onCreateNew, recentProjects, onViewTemplates, showTemplates }) => {
return (
-
+
@@ -103,10 +107,19 @@ const LandingPage = ({ onOpen, onCreateNew, recentProjects }) => {
+ onClick={onCreateNew}
+ >
New Project
+ {showTemplates && (
+
+ )}
+ {showTemplates && (
+
+ Project Templates...
+
+ )}
)
diff --git a/web/src/scripts/components/pages/LoadingPage.jsx b/web/src/scripts/components/pages/LoadingPage.jsx
new file mode 100644
index 00000000..2f91e5cd
--- /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 (
+
+ )
+ }
+}
diff --git a/web/src/scripts/components/pages/ProjectCreationPage.jsx b/web/src/scripts/components/pages/ProjectCreationPage.jsx
new file mode 100644
index 00000000..2be35f53
--- /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 (
+
+ )
+ }
+
+ 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
new file mode 100644
index 00000000..ce70c442
--- /dev/null
+++ b/web/src/scripts/components/pages/TemplatesPage.jsx
@@ -0,0 +1,178 @@
+/**
+ * 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 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',
+}
+
+export default class TemplatesPage extends Component {
+
+ renderCategory = (title, i) => {
+ const {selectedCategory, onSelectCategory} = this.props
+
+ return (
+
+ {title}
+
+ )
+ }
+
+ renderTemplate = ({name, iconUrl}, i) => {
+ const {selectedTemplateIndex, onSelectTemplate} = this.props
+
+ return (
+
+ )
+ }
+
+ render() {
+ const {selectedCategory, onBack, categories, templates} = this.props
+ const {} = this.props
+
+ return (
+
+
+
+
+
+
+
+ {categories.map(this.renderCategory)}
+
+
+ {templates.map(this.renderTemplate)}
+
+
+
+
+
+ Back
+
+
+
+ )
+ }
+}
diff --git a/web/src/scripts/components/preferences/GeneralPreferences.jsx b/web/src/scripts/components/preferences/GeneralPreferences.jsx
index e664d632..8fa92bb7 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,11 +66,11 @@ export default ({onPreferenceChange, setSystemLocationPreference, decoTheme, and
>
- {/*
@@ -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 e94b97cb..0ed3983d 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()}
this.setState({selectedCategory})
+
+ onViewLanding = () => this.setState({page: 'landing'})
+
+ onViewTemplates = () => this.setState({page: 'templates'})
+
+ onProjectNameChange = (projectName) => {
+ const {selectedCategory} = this.state
+ const sanitizedName = ProjectTemplateUtils.sanitizeProjectName(projectName, selectedCategory)
+
+ this.setState({projectName: sanitizedName})
+ }
+
+ onProjectDirectoryChange = (projectDirectory) => this.setState({projectDirectory})
+
+ onSelectTemplate = (selectedTemplateIndex) => {
+ const {selectedCategory, projectName} = this.state
+ const sanitizedName = ProjectTemplateUtils.sanitizeProjectName(projectName, selectedCategory)
+
+ this.setState({
+ page: 'projectCreation',
+ selectedTemplateIndex,
+ projectName: sanitizedName,
+ })
+ }
+
+ onCreateProject = async () => {
+ const {dispatch} = this.props
+ const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state
+ const template = ProjectTemplateConstants.TEMPLATES_FOR_CATEGORY[selectedCategory][selectedTemplateIndex]
+
+ if (!ProjectTemplateUtils.isValidProjectName(projectName, selectedCategory)) return
+
+ this.setState({page: 'loading', loadingText: 'Downloading and extracting project'})
+
+ if (selectedCategory === ProjectTemplateConstants.CATEGORY_EXPONENT) {
+ const progressCallback = (loadingText) => this.setState({loadingText})
+
+ await Exponent.createProject(projectName, projectDirectory, template, progressCallback)
+
+ selectProject({
+ type: SET_PROJECT_DIR,
+ absolutePath: new Buffer(path.join(projectDirectory, projectName)).toString('hex'),
+ isTemp: false,
+ }, dispatch)
+ } else {
+
+ // TODO actually use the selected project name & directory
+ dispatch(createProject())
+ }
+ }
+
+ renderProjectCreationPage = () => {
+ const {selectedCategory, selectedTemplateIndex, projectName, projectDirectory} = this.state
+
+ return (
+
+ )
+ }
+
+ renderTemplatesPage = () => {
+ const {selectedCategory} = this.state
+
+ return (
+
+ )
+ }
+
+ renderLoadingPage = () => {
+ const {loadingText} = this.state
+
+ return (
+
+ )
+ }
+
+ renderLandingPage = () => {
const {recentProjects} = this.state
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}
+ showTemplates={SHOW_PROJECT_TEMPLATES}
+ />
+ )
+ }
+
+ renderPage = () => {
+ const {page} = this.state
+
+ switch (page) {
+ case 'templates':
+ return this.renderTemplatesPage()
+ case 'projectCreation':
+ return this.renderProjectCreationPage()
+ case 'loading':
+ return this.renderLoadingPage()
+ default:
+ return this.renderLandingPage()
+ }
+ }
+
+ render() {
+ return (
+
+ {this.renderPage()}
+
)
}
}
diff --git a/web/src/scripts/containers/Root/Router.jsx b/web/src/scripts/containers/Root/Router.jsx
index 663c926b..8627b546 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 8a90d942..e59c65c9 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,30 @@ 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/")
+ }
+
+ openCreateExponentSlackAccount = () => {
+ shell.openExternal("https://slack.getexponent.com/")
+ }
+
launchSimulatorOfType = (simInfo, platform) => {
if (this.props.packagerIsOff) {
this.props.dispatch(runPackager())
@@ -144,18 +160,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 +181,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})
@@ -186,7 +201,6 @@ class WorkspaceToolbar extends Component {
this.props.dispatch(setConsoleVisibility(!consoleVisible))
}
-
toggleRightPane = (content) => {
const {rightSidebarVisible} = this.props
@@ -194,15 +208,42 @@ 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},
+ {text: 'Create Slack Account', action: this.openCreateExponentSlackAccount},
+ ]
return (
-
+
+
+
@@ -210,7 +251,7 @@ class WorkspaceToolbar extends Component {
menuType={'platform'}
offset={dropdownMenuOffset}
onVisibilityChange={this.setDiscussMenuVisibility}
- renderContent={this.renderDropdownMenu}
+ renderContent={this.renderDropdownMenu.bind(this, decoSlackOptions)}
>
+
+ {isExponentProject && (
+
+
+
+
+
+ )}
)
}
@@ -308,7 +365,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/ipc/ipcActionEmitter.js b/web/src/scripts/ipc/ipcActionEmitter.js
index e176da5a..9dd5a9cc 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/reducers/fileReducer.js b/web/src/scripts/reducers/fileReducer.js
index 4efe04a0..654ed7c7 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/Exponent.js b/web/src/scripts/utils/Exponent.js
new file mode 100644
index 00000000..6ec257b4
--- /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/FlowUtils.js b/web/src/scripts/utils/FlowUtils.js
index 04d3dfec..6a70268a 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) {
diff --git a/web/src/scripts/utils/ProjectTemplateUtils.js b/web/src/scripts/utils/ProjectTemplateUtils.js
new file mode 100644
index 00000000..37d486db
--- /dev/null
+++ b/web/src/scripts/utils/ProjectTemplateUtils.js
@@ -0,0 +1,48 @@
+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})
+}
+
+export const detectTemplate = (rootPath) => {
+ const exponentJSON = path.resolve(rootPath, 'exp.json')
+
+ try {
+ fs.statSync(exponentJSON)
+
+ return ProjectTemplateConstants.CATEGORY_EXPONENT
+ } catch (e) {
+ ;
+ }
+
+ 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/src/scripts/utils/selectProject.js b/web/src/scripts/utils/selectProject.js
new file mode 100644
index 00000000..ad2dd876
--- /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,
+ }))
+}
diff --git a/web/webpack.config.js b/web/webpack.config.js
index a0a40f66..b5a2bc4c 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 93c7cfb7..7fb17e13 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")