Jekyll Initial Commit

This commit is contained in:
David Erwin
2017-05-24 16:48:00 -04:00
parent a1a78334ea
commit 69b19d2c91
110 changed files with 13644 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
# Plugins
We can build plugins to extend the client side functionality of Talk.
The ultimate goal of our client side plugin architecture is to allow developers
to build on existing concepts without needing to understand core code while
providing complete power and flexibility of javascript, css and html.
* [Plugin Architecture](#plugin-architecture)
* Using our building block components
* [Reactions](#reactions)
* [Styling](#styling-plugins)
Advanced users will quickly realize that our plugins have complete access to core code. If you would like to write advanced plugins that reach outside of our published API as described in this document, please see [our notes on experimental plugins](/plugins/EXPERIMENTAL.md).
Under the hood our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript.
## Plugin Architecture
The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`.
### The Client Folder
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
```
my-plugin/
├── client/
│ └── index.js <-- index for client side functionality
├── server/
└── index.js <-- base plugin index
```
For now our base plugin `index.js` file should look like this:
```js
export default {
// We will add more here later.
};
```
### Creating a Component
We can add our components (or any other javascript code) within the `client` folder.
```
my-plugin/
├── client/
│ ├── MyComponent.js
│ └── index.js
├── server/
└── index.js
```
Our component could look like this:
```js
import React, {Component} from 'react';
class MyButton extends Component {
render() {
return <button>My Button</button>;
}
}
export default MyButton;
```
Here we create a component that renders a `button`. Now that we created our component we need to specify where it should get injected within Talk!
To tell Talk where that Component should get injected we need to specify which *Slots* to insert it into.
```js
import React from 'react';
export default = () => <button>My Button</button>;
```
### Slots
In Talk we have defined specific *Slots* where we can inject components.
Here is how we specify our slots config in `my-plugin/index.js`
```js
import MyButton from './MyButton';
export default {
slots: {
commentDetail: [MyButton]
}
};
```
Here Im specifying that the MyComponent Component will take place within the `commentDetail` in Talk.
`commentDetail` its a specific slot in the CommentStream. It means that it will be embedded inside de comment detail.
Slots properties take an`Array` so we can add as many components as we want.
## Building Blocks (TBD)
`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.`
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api.
## Reactions
Reactions provide users the ability to 'like', 'respect', etc... comments.
Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples.
### Building Reactions
#### Our `client/index.js` :
```js
import LoveButton from './LoveButton';
export default {
slots: {
commentReactions: [LoveButton]
}
};
```
In this example we add our reaction component to the `commentReaction` Slot
#### Our Reaction component:
```js
import React from 'react';
import {withReaction} from 'coral-plugin-api';
class LoveButton extends React.Component {
handleClick = () => {
const {
postReaction,
deleteReaction,
alreadyReacted
} = this.props;
if (alreadyReacted()) {
deleteReaction();
} else {
postReaction();
}
};
render() {
const {count} = this.props;
return (
<button onClick={this.handleClick}>
<span>Love</span>
<span>{count > 0 && count}</span>
</button>
);
}
}
export default withReaction('love')(LoveButton);
```
This feature introduces `withReaction` HOC. `withReaction` takes, as argument, a reaction string and it allows our component to receive specific props for handling reactions.
* `postReaction` - Posts the reaction
* `deleteReaction` - Removes the reaction
* `alreadyReacted` - A function that returns a boolean.
* `count` - The reaction count
For full reference: Please, check `coral-plugin-love`: [LoveButton.js](https://github.com/coralproject/talk/blob/master/plugins/coral-plugin-love/client/LoveButton.js)
### Comment Stream
Comment streams may be created with filtering and ordering in place:
* filter by user
* filter by tag
* sort by date ascending / descending
### Comment Commit hooks
// docs for the pre/post comment submit commit hooks
### Mod Queues
Moderation queues can be added via configuration objects passed in through plugins.
Basic mod queues will resemble the current moderation queues but can be generated from different lists of comments.
* filter by user tag
* filter by comment tag
* filter by comment status
* Custom queries (paired with back end plugins that provide queries to get the data)
#### Advanced mod queues
Advanced mod queues can be created giving plugin authors the power to create the cards that appear in the queue, create actions and custom buttons, etc...
### Custom Configuration
Plugins may rely on configuration options that admins/moderators can set in the Configuration section.
Basic settings can be added via json configuration in a plugin.
* Setting headline
* Setting description
* Setting input type
* Default value
* Variable name
#### Advanced Custom Configuration (low prioritiy)
Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration.
## Styling Plugins
Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk!
##### My Component
```js
import styles from './style.css';
class MyCoralButton extends Component {
render() {
return <button className={styles.button}>My Button</button>;
}
}
````
Our `style.css` should could look like this.
```css
.button {
background: coral;
border-radius: 3px;
}
```
## Plugin Hooks
The plugins injected in the CommentBox such as `commentInputDetailArea` will inherit through props tools for handling hooks.
### Available hook types:
`preSubmit` : To perform actions before submitting the comment.
`postSubmit` : To perform actions after submitting the comment.
### Register Hooks
`registerHook` is a function that takes: the hook type, a hook function and returns the hook data.
#### Usage:
```js
this.addCommentTagHook = this.props.registerHook('postSubmit', (data) => {
const {comment} = data.createComment;
this.props.addCommentTag({
id: comment.id,
tag: 'OFF_TOPIC'
});
});
```
### Unregister Hooks
`unregisterHook` will remove the hook.
```js
this.props.unregisterHook(this.addCommentTagHook);
```
### The server folder and the index file
Read more about the `/server` and how to extend Talk here.
[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md)
+102
View File
@@ -0,0 +1,102 @@
# Experimental plugins
Talk plugins are, in essence, small programs that hook into the core application in a variety of ways. Ultimately, this code can do anything that javascript is capable of. In addition, plugins can import any core code to hook into talk at any level.
If you want to write plugins that integrate with core code beyond the api described in [PLUGINS.md](PLUGINS.md), please keep the following things in mind:
* core code may change and break your plugin
* you may introduce inefficiencies with your plugin that could hurt performance/crash Talk
* you may cause bugs in other areas of Talk
If you'd like to build a supported plugin but don't have the hooks you need, please file an issue on this repo and we can discuss deepening the supported plugin api!
With that said, here's some of the prime experimental integration points:
## Reducers and Actions : Redux
Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions.
```js
import MyButton from './MyButton';
import reducer from './reducer';
export default {
slots: {
commentDetail: [MyButton],
},
reducer
};
```
## Import Actions from Talk
We can easily trigger `Talk` actions in our plugin Components.
```js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
class MyButton extends Component {
render() {
return <button onClick={this.props.addTag('MY_TAG')}>My Button</button>;
}
}
const mapStateToProps = ({commentBox}) => ({commentBox});
const mapDispatchToProps = dispatch =>
bindActionCreators({addTag, removeTag}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
```
## ESlint and Babel
In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own!
While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file.
#### `.eslintrc.json`
```json
{
"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"] }]
}
}
````
#### `. babelrc `
```json
{
"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"
]
}
````
+163
View File
@@ -0,0 +1,163 @@
# Plugins
The talk platform ships with a plugin architecture featuring that allows
developers to:
* extend or replace server side graph, rest and auth functionality
* inject functionality into front end embeds and the admin application
* create new front end build targets for embedding
* build plugins from local folders or published via npm/yarn
* deploy plugins throughout the application lifecycle
## Basic Concepts
All plugin code lives in the `/plugin` directory.
Each plugin is provided in a single folder named after the plugin.
### Naming a Plugin
Each plugin has a name which must be globally unique. Plugins with name
collisions will not be able to be run together in an instance of Talk.
If you are creating a plugin for the open source community, we recommend the
following naming convention:
```
coral-talk-plugin-[name]
```
If you are creating a variant of a plugin for an organization, we recommend
adding the organization's name:
```
coral-talk-plugin-[name]-[organization]
```
## Plugin Registration
In order for a Plugin to be active it must be _registered_.
The parsing order for the plugin registration is as follows:
- `TALK_PLUGINS_JSON` environment variable
- `plugins.json` file
- `plugins.default.json` file
If you need to "disable all plugins", you can simply provide `{}` as the
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
### Local Plugins
The format for plugins.json looks like this:
```json
{
"server": [
"coral-plugin-respect",
"coral-plugin-facebook-auth"
],
"client": [
"coral-plugin-respect"
]
}
```
The `server` array specifies which plugins will be loaded when the server
starts. The `client` array specifies which plugins will be built into the
front end bundles.
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above config would
require a plugin from `plugins/coral-plugin-respect` and
`plugins/coral-plugin-facebook-auth`.
### Published Plugins
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
### Resolving Plugins
External plugins can be _resolved_ by running:
```bash
./bin/cli plugins reconcile
```
This achieves two things:
1. It will traverse into local plugin folders and install their dependencies.
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
fetched again unless there is a version mismatch._ This will result in the
project `package.json` and `yarn.lock` files to be modified, this is normal as
this ensures that repeated deployments (with the same config) will have the
same config, these changes should not be committed to source control.
2. It will seek out dependencies that are listed in the object notation and try
to install them from npm.
## Plugin Dependencies
You may also include additional external dependencies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Deployment Solutions
Plugins can be deployed with a production instance of Talk.
### Source
Source deployments can just modify the `plugins.json` file and include any
local plugins into the `plugins/` directory. After including the config, you
need to reconcile the plugins and build the static assets:
```bash
# get plugin dependancies and remote plugins
./bin/cli plugins reconcile
# build staic assets (including enabled client side plugins)
yarn build
```
Then the application can be started as is.
If you are working on a plugin, our changes to the plugins will be picked up
naturally by our development scripts:
```bash
# Watch for changes to client files and rebuild
yarn build-watch
```
```bash
# Watch for changes to server files and restart
yarn dev-start
```
### Docker
If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:latest-onbuild
```
Where the directory for your instance would contain a `plugins.json` file
describing the plugin requirements and a `plugins` directory containing any
other local plugins that should be included.
Onbuild triggers will execute when the image is building with your custom
configuration and will ensure that the image is ready to use by building all
assets inside the image as well.
+382
View File
@@ -0,0 +1,382 @@
# Server Plugins
### The Client Folder
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
```
my-plugin/
├── client/
│ └── ... <-- client side plugin files
├── server/
│ └── index.js <-- index for server side functionality
└── index.js <-- base plugin index
```
## Specification
Each plugin should export a single object with all hooks available on it.
_**Note: You will have access to the whole core and other plugin's typeDefs,
context, loaders, mutators, resolvers, hooks. This is intentional, as it
encourages composing plugins to merge functionality, like a Slack plugin which
provides a Slack notify context function as well as having the loader for
comments.**_
The following are the hooks available:
### GraphQL hooks
#### Field: `typeDefs`
```graphql
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
`typeDefs` should be a string that will be _merged_ with the existing type
definitions. `enum`'s will be appended to, types will be appended, and new types
will be added.
#### Field: `context`
```js
{
Slack: (context) => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
}
```
Any property provided here will be added to the context parameter available
inside all resolvers, loaders, mutators, and of course, other context based
plugins.
The top level item must accept a context for the request which it should use to
configure the context plugin before it would be mounted at `context.plugins`.
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
#### Field: `loaders`
```js
(context) => ({
People: {
load: () => db.people.find({user: context.user})
}
})
```
Loaders should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `mutators`
```js
(context) => ({
People: {
create: (name) => {
return db.people.insert({user: context.user, name});
}
}
})
```
Mutators should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `resolvers`
```js
{
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
}
```
Should return a resolver map as described in the
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
This will merge with the existing resolvers in core and from previous plugins.
#### Field: `hooks`
```js
{
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
```
Hooks here are pretty special, for each resolver field, you can specify a
pre/post hook that will execute pre and post field resolution.
If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `setupFunctions`
```js
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
},
}),
}
```
Setup functions allow you to create filters that control which pubsub.publish() events
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
### Routes
#### Field: `router`
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
```
The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications.
### Authorization middleware
The following example creates the requisite callback route and passport
strategy needed to enable Facebook Authorization:
```js
const authorization = require('middleware/authorization');
module.exports = {
router(router) {
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
res.json({people: [{name: 'SECRET PEOPLE'}]});
});
}
}
```
#### Field: `passport`
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
},
router(router) {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
}
};
```
## Full Example
Contents of `plugins.json`:
```json
{
"server": [
"people"
]
}
```
Located in `plugins/people/index.js`:
```js
module.exports = {
typeDefs: `
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
`,
context: {
Slack: () => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
},
loaders: ({user}) => ({
People: {
load: () => db.people.find({user})
}
}),
mutators: ({user}) => ({
People: {
create: (name) => {
return db.people.insert({user, name});
}
}
}),
resolvers: {
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
},
hooks: {
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) => {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
},
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
}
}
}
};
```
## API
You can access any API available inside the talk directory in a plugin by simply
importing the file relative to the talk project root. An example would be if you
wanted to import the `MetadataService`, you would simply write:
```javascript
const MetadataService = require('services/metadata');
```