From 5d28f611b272a09bbec5bd605469874cc61250ac Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 20:26:48 -0600 Subject: [PATCH 01/11] support for targeting new targets --- plugins.js | 91 +++++++++++++++++++++++++++++------------------ webpack.config.js | 15 +++++++- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/plugins.js b/plugins.js index f0c31407f..4403f89bb 100644 --- a/plugins.js +++ b/plugins.js @@ -78,7 +78,11 @@ function isInternal(name) { */ function pluginPath(name) { if (isInternal(name)) { - return path.join(__dirname, 'plugins', name); + try { + return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()}); + } catch (e) { + return undefined; + } } try { @@ -88,15 +92,8 @@ function pluginPath(name) { } } -/** - * Itterates over the plugins and gets the plugin path's, version, and name. - * - * @param {Array} plugins - * @returns {Array} - */ -function itteratePlugins(plugins) { - return plugins.map((p) => { - let plugin = {}; +class Plugin { + constructor(entry) { // This checks to see if the structure for this entry is an object: // @@ -106,23 +103,47 @@ function itteratePlugins(plugins) { // // "people" // - if (typeof p === 'object') { - plugin.name = Object.keys(p).find((name) => name !== null); - plugin.version = p[plugin.name]; - } else if (typeof p === 'string') { - plugin.name = p; - plugin.version = `file:./plugins/${plugin.name}`; + if (typeof entry === 'object') { + this.name = Object.keys(entry).find((name) => name !== null); + this.version = entry[this.name]; + } else if (typeof entry === 'string') { + this.name = entry; + this.version = `file:./plugins/${this.name}`; } else { - throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`); + throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof entry}`); } // Get the path for the plugin. - plugin.path = pluginPath(plugin.name); + this.path = pluginPath(this.name); + } - return plugin; - }); + require() { + if (typeof this.path === 'undefined') { + throw new Error(`plugin '${this.name}' is not local and is not resolvable, plugin reconsiliation may be required`); + } + + try { + this.module = require(this.path); + } catch (e) { + if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(this.name)) { + console.error(new Error(`plugin '${this.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); + throw e; + } + + console.error(new Error(`plugin '${this.name}' could not be required from '${this.path}': ${e.message}`)); + throw e; + } + } } +/** + * Itterates over the plugins and gets the plugin path's, version, and name. + * + * @param {Array} plugins + * @returns {Array} + */ +const itteratePlugins = (plugins) => plugins.map((p) => new Plugin(p)); + // Add each plugin folder to the allowed import path so that they can import our // internal dependancies. Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => { @@ -139,22 +160,20 @@ Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((p */ class PluginSection { constructor(plugins) { - this.plugins = itteratePlugins(plugins).map((plugin) => { - if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`); - } + this.required = false; + this.plugins = itteratePlugins(plugins); + } - try { - plugin.module = require(plugin.path); - } catch (e) { - if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) { - console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); - throw e; - } + require() { + if (this.required) { + return; + } + + this.required = true; + this.plugins.forEach((plugin) => { - console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`)); - throw e; - } + // Load the plugin. + plugin.require(); if (isInternal(plugin.name)) { debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`); @@ -171,6 +190,10 @@ class PluginSection { * available. */ hook(hook) { + + // Load the plugin source if we haven't already. + this.require(); + return this.plugins .filter(({module}) => hook in module) .filter((plugin) => { diff --git a/webpack.config.js b/webpack.config.js index ddd2f9577..d2cd07300 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,11 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); +const {plugins, PluginManager} = require('./plugins'); + +const pluginManager = new PluginManager(plugins); +const targetPlugins = pluginManager.section('targets').plugins; + let pluginsConfigPath; let envPlugins = path.join(__dirname, 'plugins.env.js'); @@ -61,6 +66,13 @@ const config = { path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') ]; + return entry; + }, {}), targetPlugins.reduce((entry, plugin) => { + entry[`${plugin.name}/bundle`] = [ + 'babel-polyfill', + plugin.path + ]; + return entry; }, {})), output: { @@ -127,7 +139,8 @@ const config = { plugins: [ new LicenseWebpackPlugin({ pattern: /^(MIT|ISC|BSD.*)$/, - addUrl: true + addUrl: true, + suppressErrors: true }), new Copy([ ...buildEmbeds.map((embed) => ({ From a9a24b96c0ae6cea62eb8210fa9109c53f7dd086 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 11:49:36 -0600 Subject: [PATCH 02/11] cleaned up plugins in webpack --- plugins.js | 10 +++++++--- webpack.config.js | 28 ++++++---------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/plugins.js b/plugins.js index 4403f89bb..fa17027a2 100644 --- a/plugins.js +++ b/plugins.js @@ -10,6 +10,7 @@ const PLUGINS_JSON = process.env.TALK_PLUGINS_JSON; // Add the current path to the module root. amp.addPath(__dirname); +let pluginsPath; let plugins = {}; // Try to parse the plugins.json file, logging out an error if the plugins.json @@ -22,14 +23,16 @@ try { if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); - plugins = require(envPlugins); + pluginsPath = envPlugins; } else if (fs.existsSync(customPlugins)) { debug(`Now using ${customPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8')); + pluginsPath = customPlugins; } else { debug(`Now using ${defaultPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8')); + pluginsPath = defaultPlugins; } + + plugins = require(pluginsPath); } catch (err) { if (err.code === 'ENOENT') { console.error('plugins.json and plugins.default.json not found, plugins will not be active'); @@ -249,6 +252,7 @@ class PluginManager { module.exports = { plugins, + pluginsPath, PluginManager, isInternal, pluginPath, diff --git a/webpack.config.js b/webpack.config.js index d2cd07300..3973adc6d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ const path = require('path'); -const fs = require('fs'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); @@ -10,26 +9,11 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); -const {plugins, PluginManager} = require('./plugins'); +const {plugins, pluginsPath, PluginManager} = require('./plugins'); +const manager = new PluginManager(plugins); +const targetPlugins = manager.section('targets').plugins; -const pluginManager = new PluginManager(plugins); -const targetPlugins = pluginManager.section('targets').plugins; - -let pluginsConfigPath; - -let envPlugins = path.join(__dirname, 'plugins.env.js'); -let customPlugins = path.join(__dirname, 'plugins.json'); -let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - -if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { - pluginsConfigPath = envPlugins; -} else if (fs.existsSync(customPlugins)) { - pluginsConfigPath = customPlugins; -} else { - pluginsConfigPath = defaultPlugins; -} - -console.log(`Using ${pluginsConfigPath} as the plugin configuration path`); +console.log(`Using ${pluginsPath} as the plugin configuration path`); // Edit the build targets and embeds below. @@ -89,7 +73,7 @@ const config = { { loader: 'plugins-loader', test: /\.(json|js)$/, - include: pluginsConfigPath + include: pluginsPath }, { loader: 'babel-loader', @@ -168,7 +152,7 @@ const config = { resolve: { alias: { plugins: path.resolve(__dirname, 'plugins/'), - pluginsConfig: pluginsConfigPath + pluginsConfig: pluginsPath }, modules: [ path.resolve(__dirname, 'plugins'), From 399f1e74584fd1ddaa84a7a13dee075e8aa83d01 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 16:53:22 -0600 Subject: [PATCH 03/11] simplified webpack file --- plugins.js | 1 + webpack.config.js | 100 ++++++++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/plugins.js b/plugins.js index fa17027a2..dfe3d3312 100644 --- a/plugins.js +++ b/plugins.js @@ -84,6 +84,7 @@ function pluginPath(name) { try { return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()}); } catch (e) { + console.warn(e); return undefined; } } diff --git a/webpack.config.js b/webpack.config.js index 3973adc6d..c580a0c13 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const path = require('path'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); +const _ = require('lodash'); const Copy = require('copy-webpack-plugin'); const LicenseWebpackPlugin = require('license-webpack-plugin'); const webpack = require('webpack'); @@ -15,8 +16,6 @@ const targetPlugins = manager.section('targets').plugins; console.log(`Using ${pluginsPath} as the plugin configuration path`); -// Edit the build targets and embeds below. - const buildTargets = [ 'coral-admin', 'coral-docs' @@ -26,47 +25,16 @@ const buildEmbeds = [ 'stream' ]; +//============================================================================== +// Base Webpack Config +//============================================================================== + const config = { devtool: 'cheap-module-source-map', - entry: Object.assign({}, { - 'embed': [ - 'babel-polyfill', - path.join(__dirname, 'client/coral-embed/src/index') - ] - }, buildTargets.reduce((entry, target) => { - - // Add the entry for the bundle. - entry[`${target}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', target, '/src/index') - ]; - - return entry; - }, {}), buildEmbeds.reduce((entry, embed) => { - - // Add the entry for the bundle. - entry[`embed/${embed}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - ]; - - return entry; - }, {}), targetPlugins.reduce((entry, plugin) => { - entry[`${plugin.name}/bundle`] = [ - 'babel-polyfill', - plugin.path - ]; - - return entry; - }, {})), output: { path: path.join(__dirname, 'dist'), publicPath: '/client/', - filename: '[name].js', - - // NOTE: this causes all exports to override the global.Coral, so no more - // than one bundle.js can be included on a page. - library: 'Coral' + filename: '[name].js' }, module: { rules: [ @@ -164,6 +132,10 @@ const config = { } }; +//============================================================================== +// Production configuration overrides +//============================================================================== + if (process.env.NODE_ENV === 'production') { config.plugins.push(new CompressionPlugin({ asset: '[path].gz[query]', @@ -174,4 +146,54 @@ if (process.env.NODE_ENV === 'production') { })); } -module.exports = config; +//============================================================================== +// Entries +//============================================================================== + +// Applies the base configuration to the following entries. +const applyConfig = (entries, root = {}) => _.merge({}, config, { + entry: entries.reduce((entry, {name, path}) => { + entry[name] = [ + 'babel-polyfill', + path + ]; + + return entry; + }, {}) +}, root); + +module.exports = [ + applyConfig([ + + // Load in the root embed. + { + name: 'embed', + path: path.join(__dirname, 'client/coral-embed/src/index') + } + + ], { + output: { + library: 'Coral' + } + }), + applyConfig([ + + // Load in all the targets. + ...buildTargets.map((target) => ({ + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index') + })), + + // Load in all the embeds. + ...buildEmbeds.map((embed) => ({ + name: `embed/${embed}/bundle`, + path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + })), + + // Load in all the plugin entries. + ...targetPlugins.map(({name, path}) => ({ + name: `${name}/bundle`, + path + })) + ]) +]; From 8d2c1b72472e3d0d3db5c02182a53fd60d1ab99e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 17:25:27 -0600 Subject: [PATCH 04/11] changed bundle format --- webpack.config.js | 88 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index c580a0c13..59fe9607a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const fs = require('fs'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); @@ -163,37 +164,70 @@ const applyConfig = (entries, root = {}) => _.merge({}, config, { }, root); module.exports = [ + + // Coral Embed + // applyConfig([ + + // // Load in the root embed. + // { + // name: 'embed', + // path: path.join(__dirname, 'client/coral-embed/src/index') + // } + + // ], { + // output: { + // library: 'Coral' + // } + // }), + + // All framework targets/embeds/plugins. applyConfig([ - // Load in the root embed. - { - name: 'embed', - path: path.join(__dirname, 'client/coral-embed/src/index') - } + // // Load in all the targets. + // ...buildTargets.map((target) => ({ + // name: `${target}/bundle`, + // path: path.join(__dirname, 'client/', target, '/src/index') + // })), - ], { - output: { - library: 'Coral' - } - }), - applyConfig([ - - // Load in all the targets. - ...buildTargets.map((target) => ({ - name: `${target}/bundle`, - path: path.join(__dirname, 'client/', target, '/src/index') - })), - - // Load in all the embeds. - ...buildEmbeds.map((embed) => ({ - name: `embed/${embed}/bundle`, - path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - })), + // // Load in all the embeds. + // ...buildEmbeds.map((embed) => ({ + // name: `embed/${embed}/bundle`, + // path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + // })), // Load in all the plugin entries. - ...targetPlugins.map(({name, path}) => ({ - name: `${name}/bundle`, - path - })) + ...targetPlugins.reduce((entries, plugin) => { + + // Introspect the path to find a targets folder. + let folder = path.dirname(plugin.path); + let files = fs.readdirSync(folder); + + // While the folder does not contain the targets folder... + while (!files.includes('targets')) { + + // Try to go up a folder. + folder = path.normalize(path.join(folder, '..')); + + // And as long as we haven't gone too high + if (!(folder.includes(path.join(__dirname, 'node_modules')) || !folder.includes(path.join(__dirname, 'plugins')))) { + throw new Error(`target plugin ${plugin.name} does not have a 'targets' folder`); + } + + files = fs.readdirSync(folder); + } + + // List all targets available in that folder. + folder = path.join(folder, 'targets'); + + let targets = fs.readdirSync(folder); + if (targets.length === 0) { + throw new Error(`target plugin ${plugin.name} has no targets in it's target folder ${folder}`); + } + + return entries.concat(targets.map((target) => ({ + name: `plugin/${plugin.name}/${target}/bundle`, + path: path.join(folder, target, 'index') + }))); + }, []) ]) ]; From 99174565faa5d0c797bc30242909622e3fc6f31b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 8 Jun 2017 17:32:01 -0600 Subject: [PATCH 05/11] fixes to webpack config (uncommenting) --- webpack.config.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 59fe9607a..94ecad27a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -166,34 +166,34 @@ const applyConfig = (entries, root = {}) => _.merge({}, config, { module.exports = [ // Coral Embed - // applyConfig([ + applyConfig([ - // // Load in the root embed. - // { - // name: 'embed', - // path: path.join(__dirname, 'client/coral-embed/src/index') - // } + // Load in the root embed. + { + name: 'embed', + path: path.join(__dirname, 'client/coral-embed/src/index') + } - // ], { - // output: { - // library: 'Coral' - // } - // }), + ], { + output: { + library: 'Coral' + } + }), // All framework targets/embeds/plugins. applyConfig([ // // Load in all the targets. - // ...buildTargets.map((target) => ({ - // name: `${target}/bundle`, - // path: path.join(__dirname, 'client/', target, '/src/index') - // })), + ...buildTargets.map((target) => ({ + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index') + })), - // // Load in all the embeds. - // ...buildEmbeds.map((embed) => ({ - // name: `embed/${embed}/bundle`, - // path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - // })), + // Load in all the embeds. + ...buildEmbeds.map((embed) => ({ + name: `embed/${embed}/bundle`, + path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + })), // Load in all the plugin entries. ...targetPlugins.reduce((entries, plugin) => { From 74ad3c3a35bc11ca806ba9db3285a0edc3230937 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 18:35:53 +0700 Subject: [PATCH 06/11] Refactor all reactions and encapsulate server code --- plugin-api/beta/server/getReactionConfig.js | 113 +++++++++++ plugin-api/beta/server/index.js | 3 + .../coral-plugin-like/client/LikeButton.js | 55 ++++++ .../client/components/LikeButton.js | 86 -------- .../client/containers/LikeButton.js | 186 ------------------ plugins/coral-plugin-like/client/index.js | 4 +- .../{components/style.css => styles.css} | 9 +- .../client/translations.json | 10 - .../coral-plugin-like/client/translations.yml | 9 + plugins/coral-plugin-like/index.js | 38 +--- .../coral-plugin-like/server/typeDefs.graphql | 70 ------- .../coral-plugin-love/client/LoveButton.js | 23 ++- plugins/coral-plugin-love/client/index.js | 2 +- plugins/coral-plugin-love/client/styles.css | 37 ++-- .../client/translations.json | 10 - .../coral-plugin-love/client/translations.yml | 9 + plugins/coral-plugin-love/index.js | 38 +--- .../coral-plugin-love/server/typeDefs.graphql | 70 ------- .../client}/Icon.js | 0 .../client/RespectButton.js | 57 ++++++ .../client/components/Icon.js | 6 - .../client/components/RespectButton.js | 69 ------- .../client/containers/RespectButton.js | 163 --------------- plugins/coral-plugin-respect/client/index.js | 6 +- .../{components/style.css => styles.css} | 4 +- .../client/translations.json | 10 - .../client/translations.yml | 9 + plugins/coral-plugin-respect/index.js | 43 +--- .../server/typeDefs.graphql | 54 ----- 29 files changed, 303 insertions(+), 890 deletions(-) create mode 100644 plugin-api/beta/server/getReactionConfig.js create mode 100644 plugin-api/beta/server/index.js create mode 100644 plugins/coral-plugin-like/client/LikeButton.js delete mode 100644 plugins/coral-plugin-like/client/components/LikeButton.js delete mode 100644 plugins/coral-plugin-like/client/containers/LikeButton.js rename plugins/coral-plugin-like/client/{components/style.css => styles.css} (89%) delete mode 100644 plugins/coral-plugin-like/client/translations.json create mode 100644 plugins/coral-plugin-like/client/translations.yml delete mode 100644 plugins/coral-plugin-like/server/typeDefs.graphql delete mode 100644 plugins/coral-plugin-love/client/translations.json create mode 100644 plugins/coral-plugin-love/client/translations.yml delete mode 100644 plugins/coral-plugin-love/server/typeDefs.graphql rename plugins/{coral-plugin-like/client/components => coral-plugin-respect/client}/Icon.js (100%) create mode 100644 plugins/coral-plugin-respect/client/RespectButton.js delete mode 100644 plugins/coral-plugin-respect/client/components/Icon.js delete mode 100644 plugins/coral-plugin-respect/client/components/RespectButton.js delete mode 100644 plugins/coral-plugin-respect/client/containers/RespectButton.js rename plugins/coral-plugin-respect/client/{components/style.css => styles.css} (95%) delete mode 100644 plugins/coral-plugin-respect/client/translations.json create mode 100644 plugins/coral-plugin-respect/client/translations.yml delete mode 100644 plugins/coral-plugin-respect/server/typeDefs.graphql diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js new file mode 100644 index 000000000..101c86782 --- /dev/null +++ b/plugin-api/beta/server/getReactionConfig.js @@ -0,0 +1,113 @@ +const wrapResponse = require('../../../graph/helpers/response'); + +function getReactionConfig(reaction) { + const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); + const REACTION = reaction.toUpperCase(); + const typeDefs = ` + enum ACTION_TYPE { + + # Represents a ${Reaction}. + ${REACTION} + } + + enum ASSET_METRICS_SORT { + + # Represents a ${Reaction}Action. + ${REACTION} + } + + input Create${Reaction}Input { + + # The item's id for which we are to create a ${reaction}. + item_id: ID! + + # The type of the item for which we are to create the ${reaction}. + item_type: ACTION_ITEM_TYPE! + } + + # ${Reaction}Action is used by users who "${reaction}" a specific entity. + type ${Reaction}Action implements Action { + + # The ID of the action. + id: ID! + + # The author of the action. + user: User + + # The time when the Action was updated. + updated_at: Date + + # The time when the Action was created. + created_at: Date + } + + type ${Reaction}ActionSummary implements ActionSummary { + + # The count of actions with this group. + count: Int + + # The current user's action. + current_user: ${Reaction}Action + } + + # A summary of counts related to all the ${Reaction}s on an Asset. + type ${Reaction}AssetActionSummary implements AssetActionSummary { + + # Number of ${reaction}s associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the ${reaction}s. + actionableItemCount: Int + } + + type Create${Reaction}Response implements Response { + + # The ${reaction} that was created. + ${reaction}: ${Reaction}Action + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] + } + + type RootMutation { + + # Creates a ${reaction} on an entity. + create${Reaction}(${reaction}: Create${Reaction}Input!): Create${Reaction}Response + } + `; + + return { + typeDefs, + resolvers: { + RootMutation: { + [`create${Reaction}`]: (_, {[reaction]: {item_id, item_type}}, {mutators: {Action}}) => { + return wrapResponse(reaction)(Action.create({item_id, item_type, action_type: REACTION})); + } + }, + }, + hooks: { + Action: { + __resolveType: { + post({action_type}) { + switch (action_type) { + case REACTION: + return `${Reaction}Action`; + } + } + } + }, + ActionSummary: { + __resolveType: { + post({action_type}) { + switch (action_type) { + case REACTION: + return `${Reaction}ActionSummary`; + } + } + } + } + } + }; +} + +module.exports = getReactionConfig; diff --git a/plugin-api/beta/server/index.js b/plugin-api/beta/server/index.js new file mode 100644 index 000000000..b32d78a45 --- /dev/null +++ b/plugin-api/beta/server/index.js @@ -0,0 +1,3 @@ +module.exports = { + getReactionConfig: require('./getReactionConfig'), +}; diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js new file mode 100644 index 000000000..fac4fdfca --- /dev/null +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -0,0 +1,55 @@ +import React from 'react'; +import styles from './styles.css'; +import {withReaction} from 'plugin-api/beta/client/hocs'; +import {t, can} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-like'; + +class LikeButton extends React.Component { + handleClick = () => { + const { + postReaction, + deleteReaction, + showSignInDialog, + alreadyReacted, + user, + } = this.props; + + // If the current user does not exist, trigger sign in dialog. + if (!user) { + showSignInDialog(); + return; + } + + // If the current user is suspended, do nothing. + if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + return; + } + + if (alreadyReacted()) { + deleteReaction(); + } else { + postReaction(); + } + }; + + render() { + const {count, alreadyReacted} = this.props; + return ( +
+ +
+ ); + } +} + +export default withReaction('like')(LikeButton); diff --git a/plugins/coral-plugin-like/client/components/LikeButton.js b/plugins/coral-plugin-like/client/components/LikeButton.js deleted file mode 100644 index 7b66a11d4..000000000 --- a/plugins/coral-plugin-like/client/components/LikeButton.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, {Component} from 'react'; -import styles from './style.css'; - -import cn from 'classnames'; -import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; -import t from 'coral-framework/services/i18n'; - -const name = 'coral-plugin-like'; - -class LikeButton extends Component { - handleClick = () => { - const {postLike, showSignInDialog, deleteAction} = this.props; - const {root: {me}, comment} = this.props; - - const myLikeActionSummary = getMyActionSummary( - 'LikeActionSummary', - comment - ); - - // If the current user does not exist, trigger sign in dialog. - if (!me) { - showSignInDialog(); - return; - } - - // If the current user is banned, do nothing. - if (me.status === 'BANNED') { - return; - } - - if (myLikeActionSummary) { - deleteAction(myLikeActionSummary.current_user.id, comment.id); - } else { - postLike({ - item_id: comment.id, - item_type: 'COMMENTS' - }); - } - }; - - render() { - const {comment} = this.props; - - if (!comment) { - return null; - } - - const myLike = getMyActionSummary('LikeActionSummary', comment); - let count = getTotalActionCount('LikeActionSummary', comment); - - return ( -
- -
- ); - } -} - -LikeButton.propTypes = { - data: React.PropTypes.object.isRequired -}; - -export default LikeButton; diff --git a/plugins/coral-plugin-like/client/containers/LikeButton.js b/plugins/coral-plugin-like/client/containers/LikeButton.js deleted file mode 100644 index 51bee1b40..000000000 --- a/plugins/coral-plugin-like/client/containers/LikeButton.js +++ /dev/null @@ -1,186 +0,0 @@ -import get from 'lodash/get'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {compose, gql, graphql} from 'react-apollo'; -import LikeButton from '../components/LikeButton'; -import withFragments from 'coral-framework/hocs/withFragments'; -import{showSignInDialog} from 'coral-framework/actions/auth'; - -const isLikeAction = (a) => a.__typename === 'LikeActionSummary'; - -const COMMENT_FRAGMENT = gql` - fragment LikeButton_updateFragment on Comment { - action_summaries { - ... on LikeActionSummary { - count - current_user { - id - } - } - } - } -`; - -const withDeleteAction = graphql( - gql` - mutation deleteAction($id: ID!) { - deleteAction(id:$id) { - errors { - translation_key - } - } - } -`, - { - props: ({mutate}) => ({ - deleteAction: (id, commentId) => { - return mutate({ - variables: {id}, - optimisticResponse: { - deleteAction: { - __typename: 'DeleteActionResponse', - errors: null - } - }, - update: (proxy) => { - const fragmentId = `Comment_${commentId}`; - - // Read the data from our cache for this query. - const data = proxy.readFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId - }); - - // Check whether we liked this comment. - const idx = data.action_summaries.findIndex(isLikeAction); - if ( - idx < 0 || - get(data.action_summaries[idx], 'current_user.id') !== id - ) { - return; - } - - data.action_summaries[idx] = { - ...data.action_summaries[idx], - count: data.action_summaries[idx].count - 1, - current_user: null - }; - - // Write our data back to the cache. - proxy.writeFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId, - data - }); - } - }); - } - }) - } -); - -const withPostLike = graphql( - gql` - mutation createLike($like: CreateLikeInput!) { - createLike(like: $like) { - like { - id - } - errors { - translation_key - } - } - } -`, - { - props: ({mutate}) => ({ - postLike: (like) => { - return mutate({ - variables: {like}, - optimisticResponse: { - createLike: { - __typename: 'CreateLikeResponse', - errors: null, - like: { - __typename: 'LikeAction', - id: 'pending' - } - } - }, - update: (proxy, mutationResult) => { - const fragmentId = `Comment_${like.item_id}`; - - // Read the data from our cache for this query. - const data = proxy.readFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId - }); - - // Add our comment from the mutation to the end. - let idx = data.action_summaries.findIndex(isLikeAction); - - // Check whether we already liked this comment. - if (idx >= 0 && data.action_summaries[idx].current_user) { - return; - } - - if (idx < 0) { - - // Add initial action when it doesn't exist. - data.action_summaries.push({ - __typename: 'LikeActionSummary', - count: 0, - current_user: null - }); - idx = data.action_summaries.length - 1; - } - - data.action_summaries[idx] = { - ...data.action_summaries[idx], - count: data.action_summaries[idx].count + 1, - current_user: mutationResult.data.createLike.like - }; - - // Write our data back to the cache. - proxy.writeFragment({ - fragment: COMMENT_FRAGMENT, - id: fragmentId, - data - }); - } - }); - } - }) - } -); - -const mapDispatchToProps = (dispatch) => - bindActionCreators({showSignInDialog}, dispatch); - -const enhance = compose( - withFragments({ - root: gql` - fragment LikeButton_root on RootQuery { - me { - status - } - } - `, - comment: gql` - fragment LikeButton_comment on Comment { - action_summaries { - ... on LikeActionSummary { - count - current_user { - id - } - } - } - }` - }), - connect(null, mapDispatchToProps), - withDeleteAction, - withPostLike -); - -export default enhance(LikeButton); diff --git a/plugins/coral-plugin-like/client/index.js b/plugins/coral-plugin-like/client/index.js index 86d20863a..68b7a2c46 100644 --- a/plugins/coral-plugin-like/client/index.js +++ b/plugins/coral-plugin-like/client/index.js @@ -1,5 +1,5 @@ -import LikeButton from './containers/LikeButton'; -import translations from './translations.json'; +import LikeButton from './LikeButton'; +import translations from './translations.yml'; export default { translations, diff --git a/plugins/coral-plugin-like/client/components/style.css b/plugins/coral-plugin-like/client/styles.css similarity index 89% rename from plugins/coral-plugin-like/client/components/style.css rename to plugins/coral-plugin-like/client/styles.css index f45df86ef..cb372fa47 100644 --- a/plugins/coral-plugin-like/client/components/style.css +++ b/plugins/coral-plugin-like/client/styles.css @@ -1,6 +1,6 @@ -.like { +.container { display: inline-block; - } +} .button { color: #2a2a2a; @@ -17,14 +17,9 @@ &.liked { color: rgb(0,134,227); - &:hover { color: rgb(0,134,227); cursor: pointer; } } } - -.icon { - padding: 0 5px; -} diff --git a/plugins/coral-plugin-like/client/translations.json b/plugins/coral-plugin-like/client/translations.json deleted file mode 100644 index 93d73d3a2..000000000 --- a/plugins/coral-plugin-like/client/translations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "en": { - "like": "Like", - "liked": "Liked" - }, - "es": { - "like": "Me Gusta", - "liked": "Me Gustó" - } -} diff --git a/plugins/coral-plugin-like/client/translations.yml b/plugins/coral-plugin-like/client/translations.yml new file mode 100644 index 000000000..c534663af --- /dev/null +++ b/plugins/coral-plugin-like/client/translations.yml @@ -0,0 +1,9 @@ +en: + coral-plugin-like: + like: Like + liked: Liked +es: + coral-plugin-like: + like: Me Gusta + liked: Me Gustó + diff --git a/plugins/coral-plugin-like/index.js b/plugins/coral-plugin-like/index.js index 4fc0285f4..690577bed 100644 --- a/plugins/coral-plugin-like/index.js +++ b/plugins/coral-plugin-like/index.js @@ -1,36 +1,2 @@ -const {readFileSync} = require('fs'); -const path = require('path'); -const wrapResponse = require('../../graph/helpers/response'); - -module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), - resolvers: { - RootMutation: { - createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) { - return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'})); - } - } - }, - hooks: { - Action: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LIKE': - return 'LikeAction'; - } - } - } - }, - ActionSummary: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LIKE': - return 'LikeActionSummary'; - } - } - } - } - } -}; +const {getReactionConfig} = require('../../plugin-api/beta/server'); +module.exports = getReactionConfig('like'); diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql deleted file mode 100644 index 40c600f2f..000000000 --- a/plugins/coral-plugin-like/server/typeDefs.graphql +++ /dev/null @@ -1,70 +0,0 @@ -enum ACTION_TYPE { - - # Represents a Like. - LIKE -} - -enum ASSET_METRICS_SORT { - - # Represents a LikeAction. - LIKE -} - -input CreateLikeInput { - - # The item's id for which we are to create a like. - item_id: ID! - - # The type of the item for which we are to create the like. - item_type: ACTION_ITEM_TYPE! -} - -# LikeAction is used by users who "like" a specific entity. -type LikeAction implements Action { - - # The ID of the action. - id: ID! - - # The author of the action. - user: User - - # The time when the Action was updated. - updated_at: Date - - # The time when the Action was created. - created_at: Date -} - -type LikeActionSummary implements ActionSummary { - - # The count of actions with this group. - count: Int - - # The current user's action. - current_user: LikeAction -} - -# A summary of counts related to all the Likes on an Asset. -type LikeAssetActionSummary implements AssetActionSummary { - - # Number of likes associated with actionable types on this this Asset. - actionCount: Int - - # Number of unique actionable types that are referenced by the likes. - actionableItemCount: Int -} - -type CreateLikeResponse implements Response { - - # The like that was created. - like: LikeAction - - # An array of errors relating to the mutation that occurred. - errors: [UserError!] -} - -type RootMutation { - - # Creates a like on an entity. - createLike(like: CreateLikeInput!): CreateLikeResponse -} diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index de4c4c9e7..8dbf51a6e 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -1,8 +1,11 @@ import React from 'react'; -import {Icon} from 'coral-ui'; import styles from './styles.css'; import {withReaction} from 'plugin-api/beta/client/hocs'; import {t, can} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-love'; class LoveButton extends React.Component { handleClick = () => { @@ -35,14 +38,16 @@ class LoveButton extends React.Component { render() { const {count, alreadyReacted} = this.props; return ( - +
+ +
); } } diff --git a/plugins/coral-plugin-love/client/index.js b/plugins/coral-plugin-love/client/index.js index fa2f71159..fd7174d81 100644 --- a/plugins/coral-plugin-love/client/index.js +++ b/plugins/coral-plugin-love/client/index.js @@ -1,5 +1,5 @@ import LoveButton from './LoveButton'; -import translations from './translations.json'; +import translations from './translations.yml'; export default { translations, diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css index d48e7e28c..e16e17ca4 100644 --- a/plugins/coral-plugin-love/client/styles.css +++ b/plugins/coral-plugin-love/client/styles.css @@ -1,26 +1,25 @@ -.respect { - display: inline-block; +.container { + display: inline-block; } .button { - color: #2a2a2a; - margin: 5px 10px 5px 0px; - background: none; - padding: 0px; - border: none; - font-size: inherit; + color: #2a2a2a; + margin: 5px 10px 5px 0px; + background: none; + padding: 0px; + border: none; + font-size: inherit; + &:hover { + color: #767676; + cursor: pointer; + } + + &.loved { + color: #e52338; &:hover { - color: #767676; - cursor: pointer; - } - - &.loved { - color: #e52338; - - &:hover { - color: #e52839; - cursor: pointer; - } + color: #e52839; + cursor: pointer; } + } } diff --git a/plugins/coral-plugin-love/client/translations.json b/plugins/coral-plugin-love/client/translations.json deleted file mode 100644 index a015efa77..000000000 --- a/plugins/coral-plugin-love/client/translations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "en": { - "love": "Love", - "loved": "Loved" - }, - "es": { - "love": "Amo", - "loved": "Amé" - } -} diff --git a/plugins/coral-plugin-love/client/translations.yml b/plugins/coral-plugin-love/client/translations.yml new file mode 100644 index 000000000..06b6e5257 --- /dev/null +++ b/plugins/coral-plugin-love/client/translations.yml @@ -0,0 +1,9 @@ +en: + coral-plugin-love: + love: Love + loved: Loved +es: + coral-plugin-love: + love: Amo + loved: Amé + diff --git a/plugins/coral-plugin-love/index.js b/plugins/coral-plugin-love/index.js index 543c2a099..b57fdec39 100644 --- a/plugins/coral-plugin-love/index.js +++ b/plugins/coral-plugin-love/index.js @@ -1,36 +1,2 @@ -const {readFileSync} = require('fs'); -const path = require('path'); -const wrapResponse = require('../../graph/helpers/response'); - -module.exports = { - typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), - resolvers: { - RootMutation: { - createLove(_, {love: {item_id, item_type}}, {mutators: {Action}}) { - return wrapResponse('love')(Action.create({item_id, item_type, action_type: 'LOVE'})); - } - } - }, - hooks: { - Action: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LOVE': - return 'LoveAction'; - } - } - } - }, - ActionSummary: { - __resolveType: { - post({action_type}) { - switch (action_type) { - case 'LOVE': - return 'LoveActionSummary'; - } - } - } - } - } -}; +const {getReactionConfig} = require('../../plugin-api/beta/server'); +module.exports = getReactionConfig('love'); diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql deleted file mode 100644 index edc45e20b..000000000 --- a/plugins/coral-plugin-love/server/typeDefs.graphql +++ /dev/null @@ -1,70 +0,0 @@ -enum ACTION_TYPE { - - # Represents a Love. - LOVE -} - -enum ASSET_METRICS_SORT { - - # Represents a LoveAction. - LOVE -} - -input CreateLoveInput { - - # The item's id for which we are to create a love. - item_id: ID! - - # The type of the item for which we are to create the love. - item_type: ACTION_ITEM_TYPE! -} - -# LoveAction is used by users who "love" a specific entity. -type LoveAction implements Action { - - # The ID of the action. - id: ID! - - # The author of the action. - user: User - - # The time when the Action was updated. - updated_at: Date - - # The time when the Action was created. - created_at: Date -} - -type LoveActionSummary implements ActionSummary { - - # The count of actions with this group. - count: Int - - # The current user's action. - current_user: LoveAction -} - -# A summary of counts related to all the Loves on an Asset. -type LoveAssetActionSummary implements AssetActionSummary { - - # Number of loves associated with actionable types on this this Asset. - actionCount: Int - - # Number of unique actionable types that are referenced by the loves. - actionableItemCount: Int -} - -type CreateLoveResponse implements Response { - - # The love that was created. - love: LoveAction - - # An array of errors relating to the mutation that occurred. - errors: [UserError!] -} - -type RootMutation { - - # Creates a love on an entity. - createLove(love: CreateLoveInput!): CreateLoveResponse -} diff --git a/plugins/coral-plugin-like/client/components/Icon.js b/plugins/coral-plugin-respect/client/Icon.js similarity index 100% rename from plugins/coral-plugin-like/client/components/Icon.js rename to plugins/coral-plugin-respect/client/Icon.js diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js new file mode 100644 index 000000000..14b0ec057 --- /dev/null +++ b/plugins/coral-plugin-respect/client/RespectButton.js @@ -0,0 +1,57 @@ +import React from 'react'; +import Icon from './Icon'; +import styles from './styles.css'; +import {withReaction} from 'plugin-api/beta/client/hocs'; +import {t, can} from 'plugin-api/beta/client/services'; +import cn from 'classnames'; + +const plugin = 'coral-plugin-respect'; + +class RespectButton extends React.Component { + handleClick = () => { + const { + postReaction, + deleteReaction, + showSignInDialog, + alreadyReacted, + user, + } = this.props; + + // If the current user does not exist, trigger sign in dialog. + if (!user) { + showSignInDialog(); + return; + } + + // If the current user is suspended, do nothing. + if (!can(user, 'INTERACT_WITH_COMMUNITY')) { + return; + } + + if (alreadyReacted()) { + deleteReaction(); + } else { + postReaction(); + } + }; + + render() { + const {count, alreadyReacted} = this.props; + return ( +
+ +
+ ); + } +} + +export default withReaction('respect')(RespectButton); diff --git a/plugins/coral-plugin-respect/client/components/Icon.js b/plugins/coral-plugin-respect/client/components/Icon.js deleted file mode 100644 index c24841e97..000000000 --- a/plugins/coral-plugin-respect/client/components/Icon.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; - -export default ({className}) => ( -