diff --git a/app/main-es6/conf.js b/app/main-es6/conf.js index 0accb5c3..69ec91b7 100644 --- a/app/main-es6/conf.js +++ b/app/main-es6/conf.js @@ -10,16 +10,18 @@ const PLUGIN_PREF_DIR = `${HAIN_USER_PATH}/prefs/plugins`; const APP_PREF_DIR = `${HAIN_USER_PATH}/prefs/app`; const __PLUGIN_PREINSTALL_DIR = path.resolve('./pre_install'); -const __PLUGIN_PREUNINSTALL_FILE = path.resolve('./pre_uninstall'); +const __PLUGIN_UNINSTALL_LIST_FILE = path.resolve('./pre_uninstall'); +const __PLUGIN_UPDATE_LIST_FILE = path.resolve('./pre_update'); const INTERNAL_PLUGIN_REPO = path.join(__dirname, './plugins'); const MAIN_PLUGIN_REPO = path.resolve(`${HAIN_USER_PATH}/plugins`); const DEV_PLUGIN_REPO = path.resolve(`${HAIN_USER_PATH}/devplugins`); -const CURRENT_API_VERSION = `hain-${pkgJson.version}`; +const CURRENT_API_VERSION = 'hain-0.4.0'; const COMPATIBLE_API_VERSIONS = [ 'hain0', 'hain-0.1.0', + 'hain-0.3.0', CURRENT_API_VERSION ]; @@ -38,7 +40,8 @@ module.exports = { LOCAL_STORAGE_DIR, PLUGIN_REPOS, __PLUGIN_PREINSTALL_DIR, - __PLUGIN_PREUNINSTALL_FILE, + __PLUGIN_UNINSTALL_LIST_FILE, + __PLUGIN_UPDATE_LIST_FILE, CURRENT_API_VERSION, COMPATIBLE_API_VERSIONS }; diff --git a/app/main-es6/plugins/hain-commands/__tests__/check-update.spec.js b/app/main-es6/plugins/hain-commands/__tests__/check-update.spec.js index 3079541e..df09fcc9 100644 --- a/app/main-es6/plugins/hain-commands/__tests__/check-update.spec.js +++ b/app/main-es6/plugins/hain-commands/__tests__/check-update.spec.js @@ -1,6 +1,7 @@ 'use strict'; -jest.unmock('../check-update'); +jest.mock('got'); + const mock_got = require('got'); const checkForUpdate = require('../check-update'); diff --git a/app/main-es6/plugins/hain-package-manager/__tests__/search-client.spec.js b/app/main-es6/plugins/hain-package-manager/__tests__/search-client.spec.js index 172dc7ba..0f554ef9 100644 --- a/app/main-es6/plugins/hain-package-manager/__tests__/search-client.spec.js +++ b/app/main-es6/plugins/hain-package-manager/__tests__/search-client.spec.js @@ -1,7 +1,6 @@ 'use strict'; -jest.unmock('../search-client'); -jest.unmock('../util'); +jest.mock('got'); const mock_got = require('got'); const searchClient = require('../search-client'); diff --git a/app/main-es6/plugins/hain-package-manager/__tests__/util.spec.js b/app/main-es6/plugins/hain-package-manager/__tests__/util.spec.js index 86715c02..e13f32c8 100644 --- a/app/main-es6/plugins/hain-package-manager/__tests__/util.spec.js +++ b/app/main-es6/plugins/hain-package-manager/__tests__/util.spec.js @@ -1,7 +1,5 @@ 'use strict'; -jest.unmock('../util'); - const util = require('../util'); describe('util.js', () => { diff --git a/app/main-es6/plugins/hain-package-manager/index.js b/app/main-es6/plugins/hain-package-manager/index.js index 6e236fd7..e53c9d35 100644 --- a/app/main-es6/plugins/hain-package-manager/index.js +++ b/app/main-es6/plugins/hain-package-manager/index.js @@ -11,6 +11,7 @@ const semver = require('semver'); const Packman = require('./packman'); const searchClient = require('./search-client'); +const util = require('./util'); const COMMANDS_RE = / (install|update|uninstall|list)(\s+([^\s]+))?/i; const NAME = 'hain-package-manager (experimental)'; @@ -25,7 +26,8 @@ module.exports = (context) => { internalRepo: context.INTERNAL_PLUGIN_REPO, tempDir: path.resolve('./_temp'), installDir: context.__PLUGIN_PREINSTALL_DIR, - uninstallFile: context.__PLUGIN_PREUNINSTALL_FILE + uninstallListFile: context.__PLUGIN_UNINSTALL_LIST_FILE, + updateListFile: context.__PLUGIN_UPDATE_LIST_FILE }; const pm = new Packman(packmanOpts); const toast = context.toast; @@ -99,7 +101,7 @@ module.exports = (context) => { payload: cmdType, title: `${customName || pkgInfo.name} ` + ` ${pkgInfo.internal ? 'internal' : pkgInfo.version}` + - `${!pkgInfo.internal ? ` by ${pkgInfo.author}` : ''}` + + `${!pkgInfo.internal ? ` by ${util.parseAuthor(pkgInfo.author)}` : ''}` + ``, desc: `${pkgInfo.desc}`, group @@ -228,7 +230,7 @@ module.exports = (context) => { function uninstallPackage(packageName) { try { - pm.removePackage(packageName); + pm.uninstallPackage(packageName); toast.enqueue(`${packageName} has uninstalled, Reload plugins to take effect`, 3000); } catch (e) { toast.enqueue(e.toString()); @@ -258,7 +260,7 @@ module.exports = (context) => { logger.log(`Updating ${packageName}`); currentStatus = `Updating ${packageName}`; try { - pm.removePackage(packageName); + pm.uninstallPackageForUpdate(packageName); yield pm.installPackage(packageName, 'latest'); toast.enqueue(`${packageName} has updated, Reload plugins to take effect`, 3000); logger.log(`${packageName} has pre-installed (for update)`); diff --git a/app/main-es6/plugins/hain-package-manager/packman.js b/app/main-es6/plugins/hain-package-manager/packman.js index cfdca896..9e879d5b 100644 --- a/app/main-es6/plugins/hain-package-manager/packman.js +++ b/app/main-es6/plugins/hain-package-manager/packman.js @@ -9,12 +9,14 @@ const packageControl = require('./package-control'); const fileutil = require('../../utils/fileutil'); const fs = require('fs'); -function _createPackegeInfo(name, data, internal) { +const util = require('./util'); + +function _createPackageInfo(name, data, internal) { return { name, version: data.version || 'none', desc: data.description || '', - author: data.author || '', + author: util.parseAuthor(data.author) || '', homepage: data.homepage || '', internal: !!internal }; @@ -27,7 +29,8 @@ class Packman { this.internalRepoDir = opts.internalRepo; this.tempDir = opts.tempDir; this.installDir = opts.installDir; - this.uninstallFile = opts.uninstallFile; + this.updateListFile = opts.updateListFile; + this.uninstallListFile = opts.uninstallListFile; this.packages = []; this.internalPackages = []; @@ -44,7 +47,7 @@ class Packman { try { const fileContents = yield fileutil.readFile(packageJsonFile); const pkgJson = JSON.parse(fileContents.toString()); - const pkgInfo = _createPackegeInfo(_packageDir, pkgJson); + const pkgInfo = _createPackageInfo(_packageDir, pkgJson); self.packages.push(pkgInfo); } catch (e) { console.log(e); @@ -59,7 +62,7 @@ class Packman { try { const fileContents = yield fileutil.readFile(packageJsonFile); const pkgJson = JSON.parse(fileContents.toString()); - const pkgInfo = _createPackegeInfo(_packageDir, pkgJson, true); + const pkgInfo = _createPackageInfo(_packageDir, pkgJson, true); self.internalPackages.push(pkgInfo); } catch (e) { console.log(e); @@ -85,7 +88,7 @@ class Packman { return (this.getPackage(packageName) !== undefined); } - installPackage(packageName, versionRange, proxyAgent) { + installPackage(packageName, versionRange) { const self = this; return co(function* () { if (self.hasPackage(packageName)) @@ -94,18 +97,26 @@ class Packman { const saveDir = path.join(self.installDir, packageName); const data = yield packageControl.installPackage(packageName, versionRange, saveDir, self.tempDir); - self.packages.push(_createPackegeInfo(packageName, data)); + self.packages.push(_createPackageInfo(packageName, data)); }); } - removePackage(packageName) { + _uninstallPackage(targetListFile, packageName) { if (!this.hasPackage(packageName)) throw `Can't find a package: ${packageName}`; - fs.appendFileSync(this.uninstallFile, `${packageName}\n`); + fs.appendFileSync(targetListFile, `${packageName}\n`); lo_remove(this.packages, x => x.name === packageName); } + uninstallPackageForUpdate(packageName) { + this._uninstallPackage(this.updateListFile, packageName); + } + + uninstallPackage(packageName) { + this._uninstallPackage(this.uninstallListFile, packageName); + } + } module.exports = Packman; diff --git a/app/main-es6/plugins/hain-package-manager/util.js b/app/main-es6/plugins/hain-package-manager/util.js index 427a8a2f..8cd9f9ae 100644 --- a/app/main-es6/plugins/hain-package-manager/util.js +++ b/app/main-es6/plugins/hain-package-manager/util.js @@ -1,5 +1,7 @@ 'use strict'; +const lo_isString = require('lodash.isstring'); + function hasCompatibleAPIKeywords(apiVersions, keywords) { for (const keyword of keywords) { if (apiVersions.indexOf(keyword) >= 0) @@ -8,4 +10,10 @@ function hasCompatibleAPIKeywords(apiVersions, keywords) { return false; } -module.exports = { hasCompatibleAPIKeywords }; +function parseAuthor(author) { + if (lo_isString(author)) + return author; + return author.name; +} + +module.exports = { hasCompatibleAPIKeywords, parseAuthor }; diff --git a/app/main-es6/server/server.js b/app/main-es6/server/server.js index d49aaf07..478b2ee7 100644 --- a/app/main-es6/server/server.js +++ b/app/main-es6/server/server.js @@ -193,6 +193,11 @@ rpc.on('renderPreview', (evt, msg) => { sendmsg('renderPreview', { ticket, pluginId, id, payload }); }); +rpc.on('buttonAction', (evt, msg) => { + const { pluginId, id, payload } = msg; + sendmsg('buttonAction', { pluginId, id, payload }); +}); + rpc.define('close', function* () { app.close(); }); diff --git a/app/main-es6/worker/plugins.js b/app/main-es6/worker/plugins.js index b69c76d1..844ecdce 100644 --- a/app/main-es6/worker/plugins.js +++ b/app/main-es6/worker/plugins.js @@ -121,7 +121,8 @@ module.exports = (workerContext) => { DEV_PLUGIN_REPO: conf.DEV_PLUGIN_REPO, INTERNAL_PLUGIN_REPO: conf.INTERNAL_PLUGIN_REPO, __PLUGIN_PREINSTALL_DIR: conf.__PLUGIN_PREINSTALL_DIR, - __PLUGIN_PREUNINSTALL_FILE: conf.__PLUGIN_PREUNINSTALL_FILE, + __PLUGIN_UNINSTALL_LIST_FILE: conf.__PLUGIN_UNINSTALL_LIST_FILE, + __PLUGIN_UPDATE_LIST_FILE: conf.__PLUGIN_UPDATE_LIST_FILE, CURRENT_API_VERSION: conf.CURRENT_API_VERSION, COMPATIBLE_API_VERSIONS: conf.COMPATIBLE_API_VERSIONS, // Utilities @@ -170,19 +171,25 @@ module.exports = (workerContext) => { logger.log('startup: end'); } - function removeUninstalledPlugins() { - const listFile = conf.__PLUGIN_PREUNINSTALL_FILE; + function removeUninstalledPlugins(listFile, removeData) { if (!fs.existsSync(listFile)) return; try { const contents = fs.readFileSync(listFile, { encoding: 'utf8' }); const targetPlugins = contents.split('\n').filter((val) => (val && val.trim().length > 0)); - const repoDir = conf.MAIN_PLUGIN_REPO; for (const packageName of targetPlugins) { - const packageDir = path.join(repoDir, packageName); + const packageDir = path.join(conf.MAIN_PLUGIN_REPO, packageName); fse.removeSync(packageDir); + + if (removeData) { + const storageDir = path.join(conf.LOCAL_STORAGE_DIR, packageName); + const prefFile = path.join(conf.PLUGIN_PREF_DIR, packageName); + fse.removeSync(storageDir); + fse.removeSync(prefFile); + } + logger.log(`${packageName} has uninstalled successfully`); } fse.removeSync(listFile); @@ -211,7 +218,8 @@ module.exports = (workerContext) => { } function* initialize() { - removeUninstalledPlugins(); + removeUninstalledPlugins(conf.__PLUGIN_UNINSTALL_LIST_FILE, true); + removeUninstalledPlugins(conf.__PLUGIN_UPDATE_LIST_FILE, false); yield movePreinstalledPlugins(); const ret = pluginLoader.loadPlugins(generatePluginContext); @@ -256,9 +264,7 @@ module.exports = (workerContext) => { try { plugin.search(_query, pluginResponse); } catch (e) { - logger.log(e); - if (e.stack) - logger.log(e.stack); + logger.log(e.stack || e); } } @@ -293,6 +299,19 @@ module.exports = (workerContext) => { } } + function buttonAction(pluginId, id, payload) { + if (plugins[pluginId] === undefined) + return; + const buttonActionFunc = plugins[pluginId].buttonAction; + if (buttonActionFunc === undefined) + return; + try { + buttonActionFunc(id, payload); + } catch (e) { + logger.log(e.stack || e); + } + } + function getPrefIds() { return pluginPrefIds; } @@ -336,10 +355,11 @@ module.exports = (workerContext) => { searchAll, execute, renderPreview, + buttonAction, getPrefIds, getPreferences, updatePreferences, commitPreferences, resetPreferences }; -}; +}; \ No newline at end of file diff --git a/app/main-es6/worker/worker.js b/app/main-es6/worker/worker.js index 25653319..2ba9d005 100644 --- a/app/main-es6/worker/worker.js +++ b/app/main-es6/worker/worker.js @@ -88,6 +88,10 @@ const msgHandlers = { }; plugins.renderPreview(pluginId, id, payload, render); }, + buttonAction: (_payload) => { + const { pluginId, id, payload } = _payload; + plugins.buttonAction(pluginId, id, payload); + }, getPluginPrefIds: (payload) => { const prefIds = plugins.getPrefIds(); procMsg.send('on-get-plugin-pref-ids', prefIds); diff --git a/app/package.json b/app/package.json index 03702ec2..b122cc2a 100644 --- a/app/package.json +++ b/app/package.json @@ -1,10 +1,14 @@ { "name": "hain", - "version": "0.3.0", + "version": "0.4.0", "description": "An `alt+space` launcher for Windows, built with Electron", "main": "main-es6/main.js", "author": "Heejin Lee ", "license": "MIT", + "jest": { + "automock": false, + "testEnvironment": "node" + }, "dependencies": { "application-config-path": "^0.1.0", "co": "^4.6.0", diff --git a/app/renderer-jsx/app.jsx b/app/renderer-jsx/app.jsx index 9b23cc46..8b50b9d0 100644 --- a/app/renderer-jsx/app.jsx +++ b/app/renderer-jsx/app.jsx @@ -7,6 +7,7 @@ const lo_map = require('lodash.map'); const lo_reject = require('lodash.reject'); const lo_clamp = require('lodash.clamp'); const lo_isString = require('lodash.isstring'); +const lo_assign = require('lodash.assign'); const React = require('react'); const ReactDOM = require('react-dom'); @@ -19,7 +20,7 @@ const Ticket = require('./ticket'); const searchTicket = new Ticket(); const previewTicket = new Ticket(); -import { TextField, Avatar, SelectableContainerEnhance, List, ListItem, Subheader, FontIcon } from 'material-ui'; +import { TextField, Avatar, SelectableContainerEnhance, List, ListItem, Subheader, FontIcon, IconButton } from 'material-ui'; import MuiThemeProvider from 'material-ui/lib/MuiThemeProvider'; import getMuiTheme from 'material-ui/lib/styles/getMuiTheme'; import { Notification } from 'react-notification'; @@ -314,6 +315,39 @@ class AppContainer extends React.Component { this.refs.query.focus(); } + displayRightButton(i) { + const defaultConfig = { + className: 'fa fa-info', + color: '#009688', + hoverColor: '#00695c', + tooltip: '' + }; + const result = this.state.results[i]; + if (!result.button) { + return null; + } + const btnConfig = lo_assign({}, defaultConfig, result.button); + const fontIcon = + ; + return ; + } + + handleRightButtonClick(result, evt) { + evt.stopPropagation(); + const pluginId = result.pluginId; + const id = result.id; + const payload = result.payload; + rpc.send('buttonAction', { pluginId, id, payload }); + } + parseIconUrl(iconUrl) { if (!lo_isString(iconUrl)) { return null; @@ -335,6 +369,7 @@ class AppContainer extends React.Component { for (let i = 0; i < results.length; ++i) { const result = results[i]; const avatar = this.parseIconUrl(result.icon); + const rightIcon = this.displayRightButton(i); if (result.group !== lastGroup) { const headerId = `header.${i}`; list.push( @@ -357,6 +392,7 @@ class AppContainer extends React.Component { onClick={this.handleItemClick.bind(this, i)} onKeyDown={this.handleKeyDown.bind(this)} leftAvatar={avatar} + rightIconButton={rightIcon} /> ); } diff --git a/app/renderer-jsx/schema-form/component-selector.js b/app/renderer-jsx/schema-form/component-selector.js index a3467841..011166db 100644 --- a/app/renderer-jsx/schema-form/component-selector.js +++ b/app/renderer-jsx/schema-form/component-selector.js @@ -1,13 +1,30 @@ 'use strict'; -const components = {}; +const lo_isString = require('lodash.isstring'); +const componentInfos = []; -function select(type) { - return components[type]; +function select(schema) { + for (const componentInfo of componentInfos) { + const filter = componentInfo.filter; + const componentClass = componentInfo.componentClass; + // String matching + if (lo_isString(filter)) { + if (filter === schema.type) + return componentClass; + continue; + } + // Function matching + if (filter(schema)) + return componentClass; + } + return null; } -function inject(type, componentClass) { - components[type] = componentClass; +function inject(filter, componentClass) { + componentInfos.push({ + filter, + componentClass + }); } module.exports = { diff --git a/app/renderer-jsx/schema-form/components/array.jsx b/app/renderer-jsx/schema-form/components/array.jsx index 3fff2299..75fc8731 100644 --- a/app/renderer-jsx/schema-form/components/array.jsx +++ b/app/renderer-jsx/schema-form/components/array.jsx @@ -39,7 +39,7 @@ class ArrayComponent extends React.Component { } const childSchema = schema.items; - const ChildComponent = componentSelector.select(childSchema.type); + const ChildComponent = componentSelector.select(childSchema); if (ChildComponent === undefined) return (
Error
); diff --git a/app/renderer-jsx/schema-form/components/enum.jsx b/app/renderer-jsx/schema-form/components/enum.jsx new file mode 100644 index 00000000..cba713d3 --- /dev/null +++ b/app/renderer-jsx/schema-form/components/enum.jsx @@ -0,0 +1,43 @@ +'use strict'; + +import React from 'react'; +import { SelectField, MenuItem } from 'material-ui'; + +const utils = require('../utils'); + +class EnumComponent extends React.Component { + handleChange(evt, idx, val) { + const { onChange, path } = this.props; + onChange(path, val); + } + + render() { + const { schema, model, name, path, errors } = this.props; + let title = schema.title || name; + const description = utils.wrapDescription(schema.description); + const items = []; + + if (title !== undefined) { + title = (
{title}
); + } + + for (const itemData of schema.enum) { + const item = (); + items.push(item); + } + + return ( +
+ {title} + {description} + + {items} + +
+ ); + } +} + +module.exports = EnumComponent; diff --git a/app/renderer-jsx/schema-form/components/object.jsx b/app/renderer-jsx/schema-form/components/object.jsx index b10dfb5d..b0913755 100644 --- a/app/renderer-jsx/schema-form/components/object.jsx +++ b/app/renderer-jsx/schema-form/components/object.jsx @@ -21,7 +21,7 @@ class ObjectComponent extends React.Component { for (const childName in properties) { const property = properties[childName]; const type = property.type; - const Component = componentSelector.select(type); + const Component = componentSelector.select(property); if (Component === undefined) continue; diff --git a/app/renderer-jsx/schema-form/schema-form.jsx b/app/renderer-jsx/schema-form/schema-form.jsx index 80fb2ae5..493f64d9 100644 --- a/app/renderer-jsx/schema-form/schema-form.jsx +++ b/app/renderer-jsx/schema-form/schema-form.jsx @@ -9,6 +9,7 @@ import { Validator } from 'jsonschema'; const validator = new Validator(); const componentSelector = require('./component-selector'); +componentSelector.inject((schema) => schema.enum !== undefined, require('./components/enum')); componentSelector.inject('object', require('./components/object')); componentSelector.inject('array', require('./components/array')); componentSelector.inject('string', require('./components/string')); @@ -43,7 +44,7 @@ class SchemaForm extends React.Component { render() { const { title, schema, model } = this.state; - const FormComponent = componentSelector.select(schema.type); + const FormComponent = componentSelector.select(schema); const errors = validator.validate(model, schema).errors; let headerComponent = null; diff --git a/app/utils/__tests__/schema-defaults.spec.js b/app/utils/__tests__/schema-defaults.spec.js index 0d840079..e23397c6 100644 --- a/app/utils/__tests__/schema-defaults.spec.js +++ b/app/utils/__tests__/schema-defaults.spec.js @@ -1,7 +1,6 @@ -/* global jest, describe, it, expect */ 'use strict'; -const defaults = require.requireActual('../schema-defaults'); +const defaults = require('../schema-defaults'); describe('schema-defaults.js', () => { @@ -27,6 +26,14 @@ describe('schema-defaults.js', () => { expect(defaults({ default: objVal })).toBe(objVal); }); + it('should return first enum value if enum has provided', () => { + const schema_str = { + type: 'string', + enum: ['enum0', 'enum1'] + }; + expect(defaults(schema_str)).toBe(schema_str.enum[0]); + }); + it('should return 0 if integer type has provided', () => { const schema = { type: 'integer' diff --git a/app/utils/schema-defaults.js b/app/utils/schema-defaults.js index 4d51b60a..98eb3609 100644 --- a/app/utils/schema-defaults.js +++ b/app/utils/schema-defaults.js @@ -6,6 +6,9 @@ function defaults(schema) { if (schema.default) return schema.default; + if (schema.enum && schema.enum.length > 0) + return schema.enum[0]; + const type = schema.type; if (type === 'string') return ''; diff --git a/docs/plugin-docs.md b/docs/plugin-docs.md index 272e91e1..b1e67c36 100644 --- a/docs/plugin-docs.md +++ b/docs/plugin-docs.md @@ -33,6 +33,9 @@ Current API Version: `hain-0.3.0` * [hain-plugin-naverdictionary](https://github.com/appetizermonster/hain-plugin-naverdictionary) * [hain-plugin-best-torrent](https://github.com/kamahl19/hain-plugin-best-torrent) * [hain-plugin-npm](https://github.com/kamahl19/hain-plugin-npm) + * [hain-plugin-http-codes](https://github.com/quinnjn/hain-plugin-http-codes) +- `hain-0.3.0` + * [hain-plugin-material-colors](https://github.com/aouerfelli/hain-plugin-material-colors) ## Guides diff --git a/docs/share-your-plugins.md b/docs/share-your-plugins.md index 53acb050..efee2c13 100644 --- a/docs/share-your-plugins.md +++ b/docs/share-your-plugins.md @@ -4,7 +4,7 @@ In short, you can share your plugin by publishing it on public npmjs registry. But there are few rules. 1. You should name your plugin prefixed with `hain-plugin-`, then hpm(hain-package-manager) can find your plugin in npmjs registry. -2. You should add `hain0` keyword in your package.json, then hpm can decide api compatibility. +2. You should add `hain-0.3.0` keyword in your package.json, then hpm can decide api compatibility. ## Publishing In your plugin directory: @@ -15,4 +15,4 @@ Done. You can find your plugin in few seconds then if you read rules above properly. ## Related Docs -- [package.json Format](package-json-format.md) \ No newline at end of file +- [package.json Format](package-json-format.md) diff --git a/package.json b/package.json index 96352f23..8fad31dd 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,6 @@ "gulp-sourcemaps": "^1.6.0", "gulp-util": "^3.0.7", "gulp-zip": "^3.2.0", - "jest-cli": "^0.10.0" + "jest-cli": "^12.0.2" } }