diff --git a/.eslintignore b/.eslintignore index d506a20c2..fc0d50a25 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ client/lib **/*.html plugins/* !plugins/coral-plugin-facebook-auth -node_modules \ No newline at end of file +!plugins/coral-plugin-respect +node_modules diff --git a/.gitignore b/.gitignore index 6b7b187d9..8982613ac 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ coverage/ plugins.json plugins/* -!plugins/coral-plugin-facebook-auth \ No newline at end of file +!plugins/coral-plugin-facebook-auth +!plugins/coral-plugin-respect diff --git a/.nodemon.json b/.nodemon.json index 4c48c707e..7f7fd3d59 100644 --- a/.nodemon.json +++ b/.nodemon.json @@ -1,5 +1,5 @@ { "verbose": true, - "ignore": ["test/*", "client/*", "dist/*"], + "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"], "ext": "js,json,graphql" } diff --git a/client/coral-docs/src/index.js b/client/coral-docs/src/index.js index 5d8c49d9d..e706203a8 100644 --- a/client/coral-docs/src/index.js +++ b/client/coral-docs/src/index.js @@ -1,8 +1,8 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import {render} from 'react-dom'; import {GraphQLDocs} from 'graphql-docs'; import fetcher from './services/fetcher'; // Render the application into the DOM -ReactDOM.render(, document.querySelector('#root')); +render(, document.querySelector('#root')); diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index d970a5f35..45f2ef084 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -19,6 +19,7 @@ import FlagComment from 'coral-plugin-flags/FlagComment'; import LikeButton from 'coral-plugin-likes/LikeButton'; import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton'; import LoadMore from 'coral-embed-stream/src/LoadMore'; +import {Slot} from 'coral-framework'; import styles from './Comment.css'; @@ -157,6 +158,7 @@ class Comment extends React.Component { ? : null } +
@@ -187,6 +189,7 @@ class Comment extends React.Component { removeBest={removeBestTag} /> +
@@ -241,7 +244,8 @@ class Comment extends React.Component { showSignInDialog={showSignInDialog} reactKey={reply.id} key={reply.id} - comment={reply} />; + comment={reply} + />; }) } { diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 016536708..d9b5c8310 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -313,5 +313,5 @@ export default compose( addCommentTag, removeCommentTag, deleteAction, - queryStream + queryStream, )(Embed); diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 34145ed4d..66c19e0ab 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -41,7 +41,8 @@ class Stream extends React.Component { deleteAction, showSignInDialog, addCommentTag, - removeCommentTag + removeCommentTag, + pluginProps } = this.props; return ( @@ -67,7 +68,9 @@ class Stream extends React.Component { showSignInDialog={showSignInDialog} key={comment.id} reactKey={comment.id} - comment={comment} /> + comment={comment} + pluginProps={pluginProps} + /> ) }
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 0af50b2e2..2ef617593 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -255,13 +255,13 @@ hr { .commentActionsRight, .replyActionsRight { display: flex; justify-content: flex-end; - width: 50%; + width: 30%; } .commentActionsLeft, .replyActionsLeft { display: flex; justify-content: flex-start; float: left; - width: 50%; + width: 70%; } .comment__action-container .material-icons { diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js index fa927e58a..50efa3dcb 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-framework/actions/asset.js @@ -3,7 +3,7 @@ import coralApi from '../helpers/response'; import {addNotification} from '../actions/notification'; import {pym} from 'coral-framework'; -import I18n from '../../coral-framework/modules/i18n/i18n'; +import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; const lang = new I18n(translations); diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 01174cf05..11e7bd996 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,3 +1,5 @@ +import {gql} from 'react-apollo'; +import client from 'coral-framework/services/client'; import I18n from '../../coral-framework/modules/i18n/i18n'; import translations from './../translations'; const lang = new I18n(translations); @@ -5,6 +7,20 @@ import * as actions from '../constants/auth'; import coralApi, {base} from '../helpers/response'; import {pym} from 'coral-framework'; +const ME_QUERY = gql` + query Me { + me { + status + } + } +`; + +function fetchMe() { + return client.query({ + fetchPolicy: 'network-only', + query: ME_QUERY}); +} + // Dialog Actions export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset}); export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); @@ -52,6 +68,7 @@ export const fetchSignIn = (formData) => (dispatch) => { const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length; dispatch(signInSuccess(user, isAdmin)); dispatch(hideSignInDialog()); + fetchMe(); }) .catch(error => { if (error.metadata) { @@ -104,6 +121,7 @@ export const facebookCallback = (err, data) => dispatch => { dispatch(signInFacebookSuccess(user)); dispatch(hideSignInDialog()); dispatch(showCreateUsernameDialog()); + fetchMe(); } catch (err) { dispatch(signInFacebookFailure(err)); return; @@ -151,7 +169,10 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); export const logout = () => dispatch => { dispatch(logOutRequest()); return coralApi('/auth', {method: 'DELETE'}) - .then(() => dispatch(logOutSuccess())) + .then(() => { + dispatch(logOutSuccess()); + fetchMe(); + }) .catch(error => dispatch(logOutFailure(error))); }; diff --git a/client/coral-framework/actions/index.js b/client/coral-framework/actions/index.js new file mode 100644 index 000000000..65f7b87d3 --- /dev/null +++ b/client/coral-framework/actions/index.js @@ -0,0 +1,9 @@ +import * as authActions from './auth'; +import * as assetActions from './asset'; +import * as notificationActions from './notification'; + +export default { + authActions, + assetActions, + notificationActions, +}; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js new file mode 100644 index 000000000..3d1d61328 --- /dev/null +++ b/client/coral-framework/components/Slot.js @@ -0,0 +1,19 @@ +import React, {Component} from 'react'; +import {getSlotElements} from 'coral-framework/helpers/plugins'; + +class Slot extends Component { + render() { + const {fill, ...rest} = this.props; + return ( +
+ {getSlotElements(fill, rest)} +
+ ); + } +} + +Slot.propTypes = { + fill: React.PropTypes.string +}; + +export default Slot; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js new file mode 100644 index 000000000..e0d345db4 --- /dev/null +++ b/client/coral-framework/helpers/plugins.js @@ -0,0 +1,21 @@ +import React from 'react'; +import merge from 'lodash/merge'; +import flatten from 'lodash/flatten'; +import plugins from 'pluginsConfig'; + +export const pluginReducers = merge( + ...plugins + .filter(o => o.module.reducer) + .map(o => ({...o.module.reducer})) +); + +/** + * Returns React Elements for given slot. + */ +export function getSlotElements(slot, props = {}) { + const components = flatten(plugins + .filter(o => o.module.slots[slot]) + .map(o => o.module.slots[slot])); + return components + .map((component, i) => React.createElement(component, {...props, key: i})); +} diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js index 2900466ee..96750bffd 100644 --- a/client/coral-framework/index.js +++ b/client/coral-framework/index.js @@ -1,15 +1,16 @@ import store from './services/store'; import pym from './services/PymConnection'; import I18n from './modules/i18n/i18n'; -import * as authActions from './actions/auth'; -import * as assetActions from './actions/asset'; -import * as notificationActions from './actions/notification'; +import actions from './actions'; +import Slot from './components/Slot'; -export { +// TODO (bc): Deprecate old actions. Spreading actions is now needed. + +export default { pym, + Slot, I18n, store, - authActions, - assetActions, - notificationActions + actions, + ...actions }; diff --git a/client/coral-framework/loaders/plugins-loader.js b/client/coral-framework/loaders/plugins-loader.js new file mode 100644 index 000000000..c3534df32 --- /dev/null +++ b/client/coral-framework/loaders/plugins-loader.js @@ -0,0 +1,33 @@ +/** + * Executes `source` to retrieve plugins configuration + * and loads the `index.js` of specified plugins. + * + * Outputs a module that looks like the following: + * + * module.exports = [{plugin: string, module: object}, ...] + * + */ +const {stripIndent} = require('common-tags'); + +function getPluginList(config) { + if (config && config.client) { + return config.client.map(x => typeof x === 'string' ? x : Object.keys(x)[0]); + } + + return []; +} + +module.exports = function(source) { + this.cacheable(); + const config = this.exec(source, this.resourcePath); + const plugins = getPluginList(config).map((plugin) => `{ + module: require('${plugin}/client'), + plugin: '${plugin}' + }`); + + return stripIndent` + module.exports = [ + ${plugins.join(',')} + ]; + `; +}; diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js index 9e98c901a..fae7e60d5 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -1,9 +1,11 @@ import auth from './auth'; import user from './user'; import asset from './asset'; +import {pluginReducers} from '../helpers/plugins'; export default { auth, user, asset, + ...pluginReducers }; diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index b4a7a38df..949d37fee 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -6,9 +6,11 @@ export const client = new ApolloClient({ queryTransformer: addTypename, dataIdFromObject: (result) => { if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle - return result.__typename + result.id; // eslint-disable-line no-underscore-dangle + return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle } return null; }, networkInterface: getNetworkInterface() }); + +export default client; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js new file mode 100644 index 000000000..6d589cf23 --- /dev/null +++ b/client/coral-framework/utils/index.js @@ -0,0 +1,7 @@ +/** +* getActionSummary +* retrieves the action summary based on the type and the comment +*/ + +export const getActionSummary = (type, comment) => + comment.action_summaries.filter(a => a.__typename === type)[0]; diff --git a/graph/helpers/response.js b/graph/helpers/response.js new file mode 100644 index 000000000..ebdbc02ec --- /dev/null +++ b/graph/helpers/response.js @@ -0,0 +1,31 @@ +const errors = require('../../errors'); +const {Error: {ValidationError}} = require('mongoose'); + +/** + * Wraps up a promise to return an object with the resolution of the promise + * keyed at `key` or an error caught at `errors`. + */ + +const wrapResponse = (key) => (promise) => { + return promise.then((value) => { + let res = {}; + if (key) { + res[key] = value; + } + return res; + }).catch((err) => { + if (err instanceof errors.APIError) { + return { + errors: [err] + }; + } else if (err instanceof ValidationError) { + + // TODO: wrap this with one of our internal errors. + throw err; + } + + throw err; + }); +}; + +module.exports = wrapResponse; diff --git a/graph/hooks.js b/graph/hooks.js index 3fe1306cf..499eb60b1 100644 --- a/graph/hooks.js +++ b/graph/hooks.js @@ -1,4 +1,7 @@ -const {forEachField} = require('graphql-tools'); +const { + GraphQLObjectType, + GraphQLInterfaceType +} = require('graphql'); const debug = require('debug')('talk:graph:schema'); /** @@ -22,6 +25,33 @@ const defaultResolveFn = (source, args, context, {fieldName}) => { } }; +// This function is pretty much copied verbatim from the graphql-tools repo: +// https://github.com/apollographql/graphql-tools/blob/b12973c86e00be209d04af0184780998056051c4/src/schemaGenerator.ts#L180-L194 +// With the small alteration that we look for the `resolveType` function on the +// schema so we can wrap post hooks around it to provide additional resolve +// points. +const forEachField = (schema, fn) => { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) { + + // Here we capture the change to extract the resolve type. We pass this + // with the `isResolveType = true` to introduce the specific beheviour. + if ('resolveType' in type) { + fn(type, typeName, '__resolveType', true); + } + + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +}; + /** * Decorates the schema with pre and post hooks as provided by the Plugin * Manager. @@ -29,7 +59,7 @@ const defaultResolveFn = (source, args, context, {fieldName}) => { * @param {Array} hooks hooks to apply to the schema * @return {void} */ -const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => { +const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName, isResolveType = false) => { // Pull out the pre/post hooks from the available hooks. const { @@ -85,6 +115,48 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa return; } + // If this is a resolve type, we need to do some specific things to handle + // this type of field. + if (isResolveType) { + + // Warn if we have any pre hooks. + if (pre.length !== 0) { + throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`); + } + + // This only needs to do something if post hooks are defined. + if (post.length === 0) { + return; + } + + // Cache the original resolverType function. + let resolveType = field.resolveType; + + // Return the function to handle the resolveType hooks. + field.resolveType = (obj, context, info) => { + let type = resolveType(obj, context, info); + + // Only if a previous resolver was unable to resolve the field type do we + // progress to the hooks (in order!) to resolve the field name until we + // have resolved it. + if (typeof type !== 'undefined' && type != null) { + return type; + } + + // We will walk through the post hooks until we find the right one. This + // follows what redux does to combine existing reducers. + for (let i = 0; i < post.length; i++) { + let resolveType = post[i]; + type = resolveType(obj, context, info); + if (typeof type !== 'undefined' && type != null) { + return type; + } + } + }; + + return; + } + // Cache the original resolve function, this emulates the beheviour found in // graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425 let resolve = field.resolve; @@ -102,7 +174,7 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa await Promise.all(pre.map((pre) => pre(obj, args, context, info))); // Resolve the field. - let result = resolve(obj, args, context, info); + let result = await resolve(obj, args, context, info); // Insure all post hooks after we've resolved the field with the result // passed in as the fifth argument. diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index a474407d2..3d5ea9ad9 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -1,33 +1,6 @@ -const {Error: {ValidationError}} = require('mongoose'); -const errors = require('../../errors'); +const wrapResponse = require('../helpers/response'); const CommentsService = require('../../services/comments'); -/** - * Wraps up a promise to return an object with the resolution of the promise - * keyed at `key` or an error caught at `errors`. - */ -const wrapResponse = (key) => (promise) => { - return promise.then((value) => { - let res = {}; - if (key) { - res[key] = value; - } - return res; - }).catch((err) => { - if (err instanceof errors.APIError) { - return { - errors: [err] - }; - } else if (err instanceof ValidationError) { - - // TODO: wrap this with one of our internal errors. - throw err; - } - - throw err; - }); -}; - const RootMutation = { createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) { return wrapResponse('comment')(Comment.create({asset_id, parent_id, body})); diff --git a/package.json b/package.json index 3ea18dfcd..f4e2eb725 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "postinstall": "./bin/cli plugins reconcile --skip-remote", "start": "./bin/cli serve --jobs", "dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"", - "build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail", + "build": "NODE_ENV=production webpack -p --config webpack.config.js --bail", "build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch", "lint": "eslint bin/* .", "lint-fix": "eslint bin/* . --fix", @@ -74,6 +74,7 @@ "graphql-tools": "^0.9.0", "helmet": "^3.5.0", "inquirer": "^3.0.6", + "joi": "^10.4.1", "jsonwebtoken": "^7.3.0", "kue": "^0.11.5", "linkify-it": "^2.0.3", @@ -90,7 +91,7 @@ "parse-duration": "^0.1.1", "passport": "^0.3.2", "passport-local": "^1.0.0", - "react-apollo": "^0.10.0", + "react-apollo": "^1.0.0", "react-recaptcha": "^2.2.6", "redis": "^2.7.1", "resolve": "^1.3.2", @@ -99,8 +100,9 @@ "uuid": "^2.0.3" }, "devDependencies": { - "apollo-client": "^0.8.3", + "apollo-client": "^1.0.0", "autoprefixer": "^6.5.2", + "babel-cli": "^6.24.0", "babel-core": "^6.24.0", "babel-eslint": "^7.2.1", "babel-jest": "^19.0.0", @@ -114,10 +116,12 @@ "babel-plugin-transform-react-jsx": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.24.0", + "babel-preset-react": "^6.23.0", "babel-preset-stage-0": "^6.16.0", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-http": "^3.0.0", + "common-tags": "^1.4.0", "copy-webpack-plugin": "^4.0.0", "css-loader": "^0.27.3", "dialog-polyfill": "^0.4.4", diff --git a/plugins.default.json b/plugins.default.json index 40702cde9..b3443af48 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,5 +1,9 @@ { "server": [ + "coral-plugin-respect", "coral-plugin-facebook-auth" + ], + "client": [ + "coral-plugin-respect" ] } diff --git a/plugins.env.js b/plugins.env.js new file mode 100644 index 000000000..7be520cc0 --- /dev/null +++ b/plugins.env.js @@ -0,0 +1 @@ +module.exports = JSON.parse(process.env.TALK_PLUGINS_JSON); diff --git a/plugins.js b/plugins.js index 43ac04478..02cdb58e1 100644 --- a/plugins.js +++ b/plugins.js @@ -2,9 +2,11 @@ const fs = require('fs'); const path = require('path'); const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); +const Joi = require('joi'); +const amp = require('app-module-path'); -// Add support for require rewriting. -require('app-module-path').addPath(__dirname); +// Add the current path to the module root. +amp.addPath(__dirname); let plugins = {}; @@ -12,13 +14,13 @@ let plugins = {}; // file isn't loaded, but continuing. Else, like a parsing error, throw it and // crash the program. try { - let defaultPlugins = path.join(__dirname, 'plugins.default.json'); + let envPlugins = path.join(__dirname, 'plugins.env.js'); let customPlugins = path.join(__dirname, 'plugins.json'); - let envPluginJSON = process.env.TALK_PLUGINS_JSON; + let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - if (envPluginJSON && envPluginJSON.length > 0) { + if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); - plugins = JSON.parse(envPluginJSON); + plugins = require(envPlugins); } else if (fs.existsSync(customPlugins)) { debug(`Now using ${customPlugins} for plugins`); plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8')); @@ -34,6 +36,23 @@ try { } } +/** + * All the hooks from plugins must match the schema defined here. + */ +const hookSchemas = { + passport: Joi.func().arity(1), + router: Joi.func().arity(1), + context: Joi.object().pattern(/\w/, Joi.func().maxArity(1)), + hooks: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.object({ + pre: Joi.func(), + post: Joi.func() + }))), + loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())), + mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())), + resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())), + typeDefs: Joi.string() +}; + /** * isInternal checks to see if a given plugin is internal, and returns true * if it is. @@ -67,6 +86,12 @@ 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 = {}; @@ -96,6 +121,12 @@ function itteratePlugins(plugins) { }); } +// 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) => { + amp.enableForDir(path.dirname(plugin.path)); +})); + /** * Stores a reference to a section for a section of Plugins. */ @@ -135,6 +166,15 @@ class PluginSection { hook(hook) { return this.plugins .filter(({module}) => hook in module) + .filter((plugin) => { + + // Validate the hook. + if (hook in hookSchemas) { + Joi.assert(plugin.module[hook], hookSchemas[hook], `Plugin '${plugin.name}' failed validation for the '${hook}' hook`); + } + + return true; + }) .map((plugin) => ({ plugin, [hook]: plugin.module[hook] diff --git a/plugins/coral-plugin-respect/client/.babelrc b/plugins/coral-plugin-respect/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/coral-plugin-respect/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/coral-plugin-respect/client/.eslintrc.json b/plugins/coral-plugin-respect/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/coral-plugin-respect/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/coral-plugin-respect/client/components/Icon.js b/plugins/coral-plugin-respect/client/components/Icon.js new file mode 100644 index 000000000..c24841e97 --- /dev/null +++ b/plugins/coral-plugin-respect/client/components/Icon.js @@ -0,0 +1,6 @@ +import React from 'react'; +import cn from 'classnames'; + +export default ({className}) => ( +