diff --git a/.circleci/config.yml b/.circleci/config.yml index 507706803..2fe14ad6e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -92,7 +92,7 @@ jobs: - save_cache: key: build-cache-{{ .Branch }}-{{ .Revision }} paths: - - ./node_modules/.cache/hard-source + - ./node_modules/.cache/babel-loader - persist_to_workspace: root: . paths: dist diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js index 30fd8842c..ae60770d4 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js +++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js @@ -25,7 +25,8 @@ class QuestionBoxBuilder extends React.Component { async loadEditor() { const { default: MarkdownEditor, - } = await import('coral-framework/components/MarkdownEditor'); + } = await import(/* webpackChunkName: "markdownEditor" */ + 'coral-framework/components/MarkdownEditor'); return this.setState({ loading: false, diff --git a/docs/_config.yml b/docs/_config.yml index fe7146988..2a6fa2725 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -100,6 +100,8 @@ sidebar: children: - title: Authentication url: /integrating/authentication/ + - title: Asset Managment + url: /integrating/asset-management/ - title: Configuring the Comment Stream url: /integrating/configuring-comment-stream/ - title: Configuring the Admin diff --git a/docs/source/05-01-building-basic-plugin.md b/docs/source/05-01-building-basic-plugin.md index 77e33e29c..79d519476 100644 --- a/docs/source/05-01-building-basic-plugin.md +++ b/docs/source/05-01-building-basic-plugin.md @@ -251,6 +251,6 @@ export default withReaction('pride')(PrideButton); ```` -And that's it! You've created your first reaction button! :rainbow: +And that's it! You've created your first reaction button! 🌈 -If you would like to continue to the next part of our Plugin Tutorial, see Part 2 in the sidebar. +If you would like to continue to the next part of our Plugin Tutorial, see Customizing Plugins with Coral UI in the left sidebar. diff --git a/docs/source/integrating/asset-management.md b/docs/source/integrating/asset-management.md new file mode 100644 index 000000000..a4888f4c5 --- /dev/null +++ b/docs/source/integrating/asset-management.md @@ -0,0 +1,314 @@ +--- +title: Asset Management +permalink: /integrating/asset-management/ +--- + +One of the most frequent questions that we get asked by organizations trying to +integrate Talk is: _How do we hook our CMS up to Talk so that articles are in +sync?_ + +This guide is designed to explain the steps to take your base installation of +Talk and configure it to allow only assets pushed into it from your CMS, and +keep your URL/title in sync. We won't cover here how to install the plugin, as +it is covered in our [Plugins Overview](/talk/plugins/). + +## Why do we need to create a plugin? + +By default, Talk will use "Lazy Asset Creation" to dynamically generate Assets +in Talk in order to make it easier for lighter installations. In order to have +more strict control over this flow, we will create a plugin that will: + +1. Disable "Lazy Asset Creation" by [Overriding a Resolver](#Overriding-a-Resolver). +2. Create Assets from our CMS by [Creating a New Asset Route](#Creating-a-New-Asset-Route). +3. Facilitate updates from our CMS to keep Talk in sync by [Creating an Asset Update Route](#Creating-an-Asset-Update-Route). + +We will then modify our embed so that we can [Target the Asset](#Target-the-Asset). + +But first we should grab our basic plugin structure: + +```sh +# clone our example repo (that comes with all the code below!) +git clone https://github.com/coralproject/talk-plugin-asset-manager-example.git + +# checkout the step-1 tag that starts us off with the basic file structure of +# the plugin. +git checkout step-1 +``` + +## Overriding a Resolver + +First we'll replace the content of the `resolver.js` file with the following: + +```js +// We'll need to modify the behavior of how assets are +// "resolved" in Talk, so we override the base asset resolver +// for the RootQuery type. +module.exports = { + RootQuery: { + asset: async (root, args, ctx) => { + // We'll grab the id of the asset being requested + // such that we'll be able to lookup the asset. + const { id } = args; + if (!id) { + // If the ID isn't provided, we don't want to do + // anything. + return null; + } + + // A mouthful for sure, but we need to use the loader + // that is available on the graph context in order to + // lookup the asset by ID. + const asset = await ctx.loaders.Assets.getByID.load(id); + if (!asset) { + // If the asset can't be found, we don't want to do + // anything. + return null; + } + + // Send the asset back. + return asset; + }, + }, +}; +``` + +This serves to override the default asset resolver. You can of course, override +any other field in the schema to perform whatever action your business needs +require, including adding additional resolvers! You can refer to our +[GraphQL API Docs](/talk/reference/graphql/) to see what other fields you can +override. + +Without this, Talk will continue to use the "Lazy Asset Creation" to handle +resolving the `asset` edge, which is what we want to change. + +_Note, you can also get to this point by running `git checkout step-2`!_ + +## Creating a New Asset Route + +In order to create Assets now, we have to get our CMS to push those into Talk, +the easiest way to do this is by creating a custom route. We won't cover +specific CMS integrations, but will assume that there is some type of webhook +system you are able to utilize that will trigger when a new article is created. + +We'll replace the contents of the `router.js` file with the following: + +```js +// This file we'll create routes that will facilitate asset creation and +// updates. + +const authz = require('middleware/authorization'); + +module.exports = router => { + // We'll respond to a POST request on the following route where the request + // must have a valid ADMIN access token. + router.post( + '/api/v1/plugin/asset-manager-example', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to create + // the new Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can create the asset that was passed to us in the body of the + // request as JSON. Check the schema of the Asset model by looking at: + // https://github.com/coralproject/talk/blob/master/models/asset.js + await Assets.create(req.body); + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); +}; +``` + +This request handler when mounted on Talk will allow your CMS to send a POST +request to `${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example` with the +Asset as a JSON payload. In order to protect the endpoint from abuse, we add the +authorization middleware. This middleware essentially says, _you must be an +admin to hit this route_. We need to generate a token that can be used by your +CMS using the Talk cli tool: + +```sh +# find or create an admin user that can be used as the basis for the token +./bin/cli users list + +# create a token for the user with the given id +./bin/cli token create ${USER_ID} cms-token +``` + +You can attach the generated token to the request a few ways: + +1. HTTP Header: + + curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example \ + -XPOST \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" + +2. Query Parameter: + + curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example?access_token=${TOKEN} + -XPOST \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" + +Where `${ASSET_JSON}` is the JSON for your Asset matching the +[AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js). + +_Note, you can also get to this point by running `git checkout step-3`!_ + +## Creating an Asset Update Route + +Now imagine the situation where you decide that you want to change the url slug +of the page, or update the title, now Talk is out of sync! Let's fix that. + +Update your `router.js` to the following: + +```js +// This file we'll create routes that will facilitate asset creation and +// updates. + +const authz = require('middleware/authorization'); + +module.exports = router => { + // We'll respond to a POST request on the following route where the request + // must have a valid ADMIN access token. + router.post( + '/api/v1/plugin/asset-manager-example', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to create + // the new Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can create the asset that was passed to us in the body of the + // request as JSON. Check the schema of the Asset model by looking at: + // https://github.com/coralproject/talk/blob/master/models/asset.js + await Assets.create(req.body); + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); + + // We'll respond to a PUT request on the following route where the request + // must also have a valid ADMIN access token. + router.put( + '/api/v1/plugin/asset-manager-example/:id', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to update + // the Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can lookup the asset we're updating and apply out updates to + // the model atomically. + const asset = await Assets.findOneAndUpdate( + { id: req.params.id }, + req.body, + { + // We want to validate the model being updated. + runValidators: true, + } + ); + if (!asset) { + // The asset indicated by the ID wasn't found, let the webhook know! + return res.status(404).end(); + } + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); +}; +``` + +As you can see from the previous step of [Creating a New Asset Route](#Creating-a-New-Asset-Route) +, we have added the new `PUT` route to the router. This is a simple addition +that allows your CMS to call into Talk when the asset has updated it's title, +it's url (or really anything in the [AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js)) to keep the Talk Admin and links up to date. + +Following the previous example, you can issue the request as follows: + +```sh +curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example/${ASSET_ID} \ + -XPUT \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" +``` + +The difference from the previous curl example, is that this one changes the +method from a `POST` to a `PUT`, and we add the `${ASSET_ID}` to the end of the +url. + +_Note, you can also get to this point by running `git checkout step-4`!_ + +## Target the Asset + +Now that we have a way to create and update Assets, we now need a way to +reference it. One of the most important fields in the Asset model, is the `id`. +This `id` can be one generated from your CMS, or some other system, but must +be kept consistent. + +When you install Talk, and visit the admin panel, we can see under +`/admin/configure` in the tab for Tech Settings, an embed snippet: + +```html +
+ +``` + +We'll modify this to the following: + +```html + + +``` + +Adding the `asset_id` parameter to the render function will accomplish a very +important task. It will provide Talk with the specific ID of the asset to +associate with the displayed page. This is important because even if you update +the URL in the future, the embed will still reference the correct Asset. The +`${ASSET_ID}` should be replaced by your CMS with the correct Asset id using +your desired scripting/templating tools. + +At this point, you should have a fully built Talk plugin that can be paired with +some work on your CMS to create a fully integrated asset management pipeline! + +To view the fully completed source code, visit +https://github.com/coralproject/talk-plugin-asset-manager-example. \ No newline at end of file diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js index 5a7c34b60..b7d01feb4 100644 --- a/middleware/staticTemplate.js +++ b/middleware/staticTemplate.js @@ -1,4 +1,7 @@ const SettingsService = require('../services/settings'); +const fs = require('fs'); +const path = require('path'); +const { merge } = require('lodash'); const { BASE_URL, @@ -34,6 +37,45 @@ const attachStaticLocals = locals => { } }; +// MANIFESTS are all the manifests accessible by Talk. +const MANIFESTS = ['../dist/manifest.json', '../dist/manifest.embed.json']; + +// getManifest will retrieve the manifest files and parse the JSON. +function getManifest() { + return merge( + {}, + ...MANIFESTS.map(f => + fs.readFileSync(path.resolve(__dirname, f), 'utf8') + ).map(JSON.parse) + ); +} + +/** + * resolve is a function that can be used in templates to resolve an asset from + * the manifest. In production, the manifest is cached. + */ +const resolve = (() => { + if (process.env.NODE_ENV === 'production') { + // In production, we should attempt to load the manifest early. + const manifest = getManifest(); + + return key => `${STATIC_URL}static/${manifest[key]}`; + } + + // In dev mode, we are more forgiving and we always load the + // newest version of the manifest. + return key => { + try { + const manifest = getManifest(); + + return `${STATIC_URL}static/${manifest[key]}`; + } catch (err) { + console.warn(err); + return ''; + } + }; +})(); + module.exports = async (req, res, next) => { try { // Attach the custom css url. @@ -46,6 +88,10 @@ module.exports = async (req, res, next) => { // Always attach the locals. attachStaticLocals(res.locals); + // Resolve will help resolving paths to static files + // using the manifest. + res.locals.resolve = resolve; + // Forward the request. next(); }; diff --git a/package.json b/package.json index 924df52d6..0aa4f247b 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ "graphql-tag": "^1.2.3", "graphql-tools": "^0.10.1", "hammerjs": "^2.0.8", - "hard-source-webpack-plugin": "^0.6.0", "helmet": "3.8.2", "history": "^3.0.0", "hjson": "^3.1.1", @@ -213,6 +212,7 @@ "enzyme-adapter-react-15": "^1.0.0", "eslint": "^4.5.0", "eslint-plugin-mocha": "^4.11.0", + "extract-text-webpack-plugin": "^3.0.2", "husky": "^0.14.3", "identity-obj-proxy": "^3.0.0", "ip": "^1.1.5", @@ -221,11 +221,13 @@ "lint-staged": "^7.0.0", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", + "name-all-modules-plugin": "^1.0.1", "nightwatch": "^0.9.16", "nodemon": "^1.11.0", "selenium-standalone": "^6.11.0", "sinon": "^3.2.1", "sinon-chai": "^2.13.0", + "webpack-manifest-plugin": "^2.0.0-rc.2", "yaml-lint": "^1.0.0" }, "engines": { diff --git a/views/admin.ejs b/views/admin.ejs index 939da0dcf..1ecca390e 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -3,8 +3,6 @@