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 ( +
+
+
+ +
+
+
+
+ {text} +
+
+
+
+ ) + } +} 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 ( +
+
+ {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 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 ( +
+
+ {name} +
+
+
+ ) + } + + 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")