From 5d28f611b272a09bbec5bd605469874cc61250ac Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 7 Jun 2017 20:26:48 -0600 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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 f929059daf38d6b9539b26abd23a94659cd1b283 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 9 Jun 2017 13:23:16 -0600 Subject: [PATCH 06/17] added asset merging + url updates --- bin/cli-assets | 156 +++++++++++++++++++++++---------- errors.js | 10 ++- services/assets.js | 59 ++++++++++++- test/server/services/assets.js | 94 ++++++++++++++++++-- 4 files changed, 261 insertions(+), 58 deletions(-) diff --git a/bin/cli-assets b/bin/cli-assets index 97b5b043c..4805bead0 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -8,9 +8,12 @@ const program = require('./commander'); const parseDuration = require('ms'); const Table = require('cli-table'); const AssetModel = require('../models/asset'); +const CommentModel = require('../models/comment'); +const AssetsService = require('../services/assets'); const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); const util = require('./util'); +const inquirer = require('inquirer'); // Register the shutdown criteria. util.onshutdown([ @@ -20,65 +23,112 @@ util.onshutdown([ /** * Lists all the assets registered in the database. */ -function listAssets() { - AssetModel - .find({}) - .sort({'created_at': 1}) - .then((asset) => { - let table = new Table({ - head: [ - 'ID', - 'Title', - 'URL' - ] - }); +async function listAssets() { + try { + let assets = await AssetModel.find({}).sort({'created_at': 1}); - asset.forEach((asset) => { - table.push([ - asset.id, - asset.title ? asset.title : '', - asset.url ? asset.url : '' - ]); - }); - - console.log(table.toString()); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); + let table = new Table({ + head: [ + 'ID', + 'Title', + 'URL' + ] }); + + assets.forEach((asset) => { + table.push([ + asset.id, + asset.title ? asset.title : '', + asset.url ? asset.url : '' + ]); + }); + + console.log(table.toString()); + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } } -function refreshAssets(ageString) { - const now = new Date().getTime(); - const ageMs = parseDuration(ageString); - const age = new Date(now - ageMs); +async function refreshAssets(ageString) { + try { + const now = new Date().getTime(); + const ageMs = parseDuration(ageString); + const age = new Date(now - ageMs); - AssetModel.find({ - $or: [ - { - scraped: { - $lte: age + let assets = await AssetModel.find({ + $or: [ + { + scraped: { + $lte: age + } + }, + { + scraped: null } - }, - { - scraped: null - } - ] - }) + ] + }); - // Queue all the assets for scraping. - .then((assets) => Promise.all(assets.map(scraper.create))) + // Queue all the assets for scraping. + await Promise.all(assets.map(scraper.create)); - .then(() => { console.log('Assets were queued to be scraped'); util.shutdown(); - }) - .catch((err) => { - console.error(err); + } catch (e) { + console.error(e); util.shutdown(1); - }); + } +} + +async function updateURL(assetID, assetURL) { + try { + await AssetsService.updateURL(assetID, assetURL); + + console.log(`Asset ${assetID} was updated to have url ${assetURL}.`); + util.shutdown(); + } catch (e) { + console.error(e); + util.shutdown(1); + } +} + +async function merge(srcID, dstID) { + try { + + // Grab the assets... + let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]); + if (!srcAsset || !dstAsset) { + throw new Error('Not all assets indicated by id exist, cannot merge'); + } + + // Count the affected resources... + let srcCommentCount = await CommentModel.find({asset_id: srcID}).count(); + + console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`); + + let {confirm} = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Proceed with merge', + default: false + } + ]); + + if (confirm) { + + // Perform the merge! + await AssetsService.merge(srcID, dstID); + } else { + console.warn('Aborting merge'); + } + + util.shutdown(0); + } catch (e) { + console.error(e); + util.shutdown(1); + } } //============================================================================== @@ -95,6 +145,16 @@ program .description('queues the assets that exceed the age requested') .action(refreshAssets); +program + .command('update-url ') + .description('update the URL of an asset') + .action(updateURL); + +program + .command('merge ') + .description('merges two assets together by moving comments from src to dst and deleting the src asset') + .action(merge); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/errors.js b/errors.js index c203b1d25..87c3a2a65 100644 --- a/errors.js +++ b/errors.js @@ -168,6 +168,13 @@ const ErrCommentTooShort = new APIError('Comment was too short', { status: 400 }); +// ErrAssetURLAlreadyExists is returned when a rename operation is requested +// but an asset already exists with the new url. +const ErrAssetURLAlreadyExists = new APIError('Asset URL already exists, cannot rename', { + translation_key: 'ASSET_URL_ALREADY_EXISTS', + status: 409 +}); + module.exports = { ExtendableError, APIError, @@ -191,5 +198,6 @@ module.exports = { ErrInstallLock, ErrLoginAttemptMaximumExceeded, ErrEditWindowHasEnded, - ErrCommentTooShort + ErrCommentTooShort, + ErrAssetURLAlreadyExists }; diff --git a/services/assets.js b/services/assets.js index a86c4281f..b81513b72 100644 --- a/services/assets.js +++ b/services/assets.js @@ -1,3 +1,4 @@ +const CommentModel = require('../models/comment'); const AssetModel = require('../models/asset'); const SettingsService = require('./settings'); const domainlist = require('./domainlist'); @@ -128,9 +129,61 @@ module.exports = class AssetsService { * @param {Array} ids an array of Strings of asset_id * @return {Promise} resolves to list of Assets */ - static findMultipleById(ids) { - const query = ids.map((id) => ({id})); - return AssetModel.find(query); + static async findByIDs(ids) { + + // Find the assets. + let assets = await AssetModel.find({ + id: { + $in: ids + } + }); + + // Return them in the right order. + return ids.map((id) => assets.find((asset) => asset.id === id)); + } + + static async updateURL(id, url) { + + // Try to see if an asset already exists with the given url. + let asset = await AssetsService.findByUrl(url); + if (asset !== null) { + throw errors.ErrAssetURLAlreadyExists; + } + + // Seems that there was no other asset with the same url, try and perform + // the rename operation! An error may be thrown from this if the operation + // fails. This is ok. + await AssetModel.update({id}, {$set: {url}}); + } + + static async merge(srcAssetID, dstAssetID) { + + // Fetch both assets. + let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcAssetID, dstAssetID]); + if (!srcAsset || !dstAsset) { + throw errors.ErrNotFound; + } + + // Resolve the merge operation, this invloves moving all resources attached + // to the src asset to the dst asset, and then removing the src asset. + + // First, update all the old comments to the new asset. + await CommentModel.update({ + asset_id: srcAssetID + }, { + $set: { + asset_id: dstAssetID + } + }, { + multi: true + }); + + // Second remove the old asset. + await AssetModel.remove({ + id: srcAssetID + }); + + // That's it! } static all(skip = null, limit = null) { diff --git a/test/server/services/assets.js b/test/server/services/assets.js index e46e75576..748e177e0 100644 --- a/test/server/services/assets.js +++ b/test/server/services/assets.js @@ -1,22 +1,29 @@ const AssetModel = require('../../../models/asset'); +const CommentModel = require('../../../models/comment'); const AssetsService = require('../../../services/assets'); +const CommentsService = require('../../../services/comments'); const SettingsService = require('../../../services/settings'); +const url = require('url'); const chai = require('chai'); const expect = chai.expect; +const chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); // Use the chai should. chai.should(); +const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}}; +const defaults = {url:'http://test.com'}; + describe('services.AssetsService', () => { - beforeEach(() => { - const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}}; - const defaults = {url:'http://test.com'}; + let asset; + beforeEach(async () => { + await SettingsService.init(settings); - return SettingsService.init(settings).then(() => { - return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true}); - }); + asset = await AssetModel.findOneAndUpdate({id: '1'}, {$setOnInsert: defaults}, {upsert: true, new: true}); }); describe('#findById', ()=> { @@ -120,4 +127,79 @@ describe('services.AssetsService', () => { }); }); }); + + describe('#updateURL', () => { + + it('should change the url if the asset was found, and there was no conflict', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Update the asset. + await AssetsService.updateURL(asset.id, newURL); + + // Check that the url was updated. + let {url: databaseURL} = await AssetsService.findById(asset.id); + + expect(databaseURL).to.equal(newURL); + }); + + it('should error if the new url already exists', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Create a new asset with our new URL. + await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true}); + + return AssetsService.updateURL(asset.id, newURL).should.eventually.be.rejected; + }); + + }); + + describe('#merge', () => { + + it('should error if either the src or the dst is missing', () => { + return AssetsService.merge('not-found', asset.id).should.eventually.be.rejected; + }); + + it('should merge the assets', async () => { + let newURL = url.resolve(asset.url, '/new-url'); + + // Create a new asset with our new URL. + await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true}); + + // Create some comments on both assets. + await CommentsService.publicCreate([ + { + asset_id: '1', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '1', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '2', + body: 'This is a comment!', + status: 'ACCEPTED' + }, + { + asset_id: '2', + body: 'This is a comment!', + status: 'ACCEPTED' + } + ]); + + // Merge all the comments from asset 1 into asset 2, followed by deleting + // asset 1. + await AssetsService.merge('1', '2'); + + // Check to see if the comments are moved. + expect(await CommentModel.find({asset_id: '1'}).count()).to.equal(0); + expect(await CommentModel.find({asset_id: '2'}).count()).to.equal(4); + + // Check to see if the asset was removed. + expect(await AssetModel.findOne({id: '1'})).to.equal(null); + }); + + }); }); From cc4c943a21c7c6950b194c891b7b9ad347dfdbe5 Mon Sep 17 00:00:00 2001 From: IAmSamHankins Date: Fri, 9 Jun 2017 17:27:35 -0400 Subject: [PATCH 07/17] hover states, link styling, alignment fixes --- .../coral-admin/src/components/ui/Header.css | 2 +- .../routes/Dashboard/components/Widget.css | 22 +++++----- .../Moderation/components/CommentCount.css | 1 + .../Moderation/components/UserDetail.css | 4 +- .../routes/Moderation/components/styles.css | 44 ++++++++++++------- locales/en.yml | 4 +- 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.css b/client/coral-admin/src/components/ui/Header.css index 7cb68ecbf..d99a7f295 100644 --- a/client/coral-admin/src/components/ui/Header.css +++ b/client/coral-admin/src/components/ui/Header.css @@ -77,7 +77,7 @@ letter-spacing: .8; &:hover { - background-color: #232323; + background-color: #404040; } &.active { diff --git a/client/coral-admin/src/routes/Dashboard/components/Widget.css b/client/coral-admin/src/routes/Dashboard/components/Widget.css index 587c51ada..cf83fcbdc 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Widget.css +++ b/client/coral-admin/src/routes/Dashboard/components/Widget.css @@ -17,8 +17,9 @@ .heading { margin: 0; padding-left: 10px; - font-size: 1.5rem; - font-weight: bold; + font-size: 1.3rem; + font-weight: 600; + color: #2c2c2c; } .widgetTable { @@ -32,7 +33,8 @@ } .widgetHead p { - color: rgb(35, 102, 223); + color: #2c2c2c; + font-weight: 500; padding: 10px; text-align: left; text-transform: capitalize; @@ -47,11 +49,11 @@ } .rowLinkify { - cursor: pointer; border-bottom: 1px solid lightgrey; color: #555; height: var(--row-height); padding: 10px; + transition: background-color 200ms; } .rowLinkify:last-child { @@ -60,6 +62,7 @@ .rowLinkify:hover { background-color: #f8f8f8; + pointer: default; } .linkToAsset { @@ -68,16 +71,17 @@ } .linkToModerate { - background-color: #e0e0e0; + background-color: #BDBDBD; padding: 10px 14px; text-decoration: none; color: black; float: right; margin-left: 15px; + transition: background-color 200ms; } .linkToModerate:hover { - background-color: #ccc; + background-color: #9E9E9E; } .lede { @@ -89,14 +93,10 @@ color: #555; text-decoration: none; font-size: 1.2em; - font-weight: normal; + font-weight: 500; margin: 0; } -.assetTitle:hover { - text-decoration: underline; -} - .widgetCount { color: #555; font-size: 1.3em; diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.css b/client/coral-admin/src/routes/Moderation/components/CommentCount.css index b684d0f4a..4d24d5702 100644 --- a/client/coral-admin/src/routes/Moderation/components/CommentCount.css +++ b/client/coral-admin/src/routes/Moderation/components/CommentCount.css @@ -12,4 +12,5 @@ right: 0; margin-top: -2px; font-size: 12px; + color: white; } diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.css b/client/coral-admin/src/routes/Moderation/components/UserDetail.css index d4b5f0db7..710f84759 100644 --- a/client/coral-admin/src/routes/Moderation/components/UserDetail.css +++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.css @@ -15,7 +15,7 @@ display: flex; .stat { - margin: 0 4px 12px; + margin: 0 4px 10px 0px; } .stat:last-child { @@ -49,7 +49,7 @@ li { display: inline-block; - margin: 0 10px; + margin-right: 10px; cursor: pointer; padding: 0 10px; } diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index d10311f74..fc825eccb 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -18,14 +18,20 @@ .tab { flex: 1; - color: #EEEEEE; + color: #C0C0C0; text-transform: capitalize; font-weight: 100; - font-size: 15px; + font-size: 14px; letter-spacing: 1px; transition: border-bottom 200ms; - padding: 0px 5px; - margin-right: 30px; + transition: color 200ms; + padding: 0px 10px; + margin-right: 20px; + &:hover { + color: white; + border-bottom: solid 2px #F36451; + box-sizing: border-box; + } } .active { @@ -33,6 +39,10 @@ box-sizing: border-box; border-bottom: solid 4px #F36451; font-weight: 400; + &:hover { + border-bottom: solid 4px #F36451; + font-weight: 400; + } } .active > span { @@ -103,19 +113,18 @@ span { font-weight: 400; font-size: 15px; letter-spacing: 1px; - transition: opacity 200ms; + transition: background-color 200ms; opacity: 1; - &:hover { - opacity: .8; - cursor: pointer; - } - &:first-child { text-align: left; } &:nth-child(2) { + &:hover { + cursor: pointer; + background-color: #212121; + } span { text-align: center; text-overflow: ellipsis; @@ -245,7 +254,6 @@ span { padding: 5px; color: #262626; font-size: 14px; - margin-left: 15px; line-height: 1px; font-weight: 300; } @@ -421,17 +429,21 @@ span { .tabIcon { position: relative; - top: 2px; + top: 3px; font-size: 16px; } .username { - color: blue; - text-decoration: underline; + color: #393B44; + text-decoration: none; cursor: pointer; - + font-weight: 600; + padding: 2px 5px; + border-radius: 2px; + margin-left: -5px; + transition: background-color 200ms ease; &:hover { - background-color: rgba(255, 0, 0, .1); + background-color: #E0E0E0; } } diff --git a/locales/en.yml b/locales/en.yml index ce9abdce5..930a4eadb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -144,8 +144,8 @@ en: auto_update: "Data automatically updates every five minutes or when you Reload." comment_count: comments flags: Flags - most_flags: "Articles with the most flags" - most_conversations: "Articles with the most conversations" + most_flags: "Stories with the most flags" + most_conversations: "Stories with the most conversations" next_update: "{0} minutes until next update." no_activity: "There haven't been any comments anywhere in the last five minutes." no_flags: "There have been no flags in the last 5 minutes! Hooray!" From 74ad3c3a35bc11ca806ba9db3285a0edc3230937 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 9 Jun 2017 18:35:53 +0700 Subject: [PATCH 08/17] 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}) => ( -