diff --git a/.nodemon.json b/.nodemon.json
index 101104f4a..5e192d80c 100644
--- a/.nodemon.json
+++ b/.nodemon.json
@@ -1,6 +1,6 @@
{
"exec": "npm-run-all --parallel generate-introspection start:development",
- "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
+ "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client", "docs/*"],
"ext": "js,json,graphql,yml",
"watch": [
".",
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 ae60770d4..64593fc43 100644
--- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js
+++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js
@@ -1,39 +1,15 @@
import React from 'react';
import QuestionBox from '../../../components/QuestionBox';
-import { Icon, Spinner } from 'coral-ui';
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
import cn from 'classnames';
import styles from './QuestionBoxBuilder.css';
+import { Icon } from 'coral-ui';
+import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
const DefaultIcon = ;
-
const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
class QuestionBoxBuilder extends React.Component {
- constructor() {
- super();
-
- this.state = {
- loading: true,
- };
- }
-
- componentWillMount() {
- this.loadEditor();
- }
-
- async loadEditor() {
- const {
- default: MarkdownEditor,
- } = await import(/* webpackChunkName: "markdownEditor" */
- 'coral-framework/components/MarkdownEditor');
-
- return this.setState({
- loading: false,
- MarkdownEditor,
- });
- }
-
render() {
const {
questionBoxIcon,
@@ -41,11 +17,6 @@ class QuestionBoxBuilder extends React.Component {
onContentChange,
onIconChange,
} = this.props;
- const { loading, MarkdownEditor } = this.state;
-
- if (loading) {
- return ;
- }
return (
diff --git a/client/coral-framework/components/MarkdownEditor.js b/client/coral-framework/components/MarkdownEditor.js
index e9658ec69..ce92a2437 100644
--- a/client/coral-framework/components/MarkdownEditor.js
+++ b/client/coral-framework/components/MarkdownEditor.js
@@ -1,154 +1,11 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import SimpleMDE from 'simplemde';
-import cn from 'classnames';
-import noop from 'lodash/noop';
-import styles from './MarkdownEditor.css';
+import { Spinner } from 'coral-ui';
+import Loadable from 'react-loadable';
-const config = {
- status: false,
+const MarkdownEditor = Loadable({
+ loader: () =>
+ import(/* webpackChunkName: "markdownEditor" */
+ './loadable/MarkdownEditor'),
+ loading: Spinner,
+});
- // Do not download fontAwesome icons as we replace them with
- // material icons.
- autoDownloadFontAwesome: false,
-
- // Disable built-in spell checker as it is very rudimentary.
- spellChecker: false,
-
- toolbar: [
- {
- name: 'bold',
- action: SimpleMDE.toggleBold,
- className: styles.iconBold,
- title: 'Bold',
- },
- {
- name: 'italic',
- action: SimpleMDE.toggleItalic,
- className: styles.iconItalic,
- title: 'Italic',
- },
- {
- name: 'title',
- action: SimpleMDE.toggleHeadingSmaller,
- className: styles.iconTitle,
- title: 'Title, Subtitle, Heading',
- },
- '|',
- {
- name: 'quote',
- action: SimpleMDE.toggleBlockquote,
- className: styles.iconQuote,
- title: 'Quote',
- },
- {
- name: 'unordered-list',
- action: SimpleMDE.toggleUnorderedList,
- className: styles.iconUnorderedList,
- title: 'Generic List',
- },
- {
- name: 'ordered-list',
- action: SimpleMDE.toggleOrderedList,
- className: styles.iconOrderedList,
- title: 'Numbered List',
- },
- '|',
- {
- name: 'link',
- action: SimpleMDE.drawLink,
- className: styles.iconLink,
- title: 'Create Link',
- },
- {
- name: 'image',
- action: SimpleMDE.drawImage,
- className: styles.iconImage,
- title: 'Insert Image',
- },
- '|',
- {
- name: 'preview',
- action: SimpleMDE.togglePreview,
- className: cn(styles.iconPreview, 'no-disable'),
- title: 'Toggle Preview',
- },
- {
- name: 'side-by-side',
- action: SimpleMDE.toggleSideBySide,
- className: cn(styles.iconSideBySide, 'no-disable'),
- title: 'Toggle Side by Side',
- },
- {
- name: 'fullscreen',
- action: SimpleMDE.toggleFullScreen,
- className: cn(styles.iconFullscreen, 'no-disable'),
- title: 'Toggle Fullscreen',
- },
- '|',
- {
- name: 'guide',
- action: 'https://simplemde.com/markdown-guide',
- className: styles.iconGuide,
- title: 'Markdown Guide',
- },
- ],
-};
-
-export default class MarkdownEditor extends Component {
- textarea = null;
- editor = null;
-
- onRef = ref => (this.textarea = ref);
-
- componentDidMount() {
- this.editor = new SimpleMDE({
- ...config,
- element: this.textarea,
- });
-
- // Don't trap the key, to stay accessible.
- this.editor.codemirror.options.extraKeys['Tab'] = false;
- this.editor.codemirror.options.extraKeys['Shift-Tab'] = false;
-
- this.editor.codemirror.on('change', this.onChange);
- }
-
- componentWillReceiveProps(nextProps) {
- if (
- this.props.value !== nextProps.value &&
- nextProps.value !== this.editor.value()
- ) {
- this.editor.value(nextProps.value);
- }
- }
-
- componentDidUpdate() {
- // Workaround empty render issue.
- // https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
- this.editor.codemirror.refresh();
- }
-
- componentWillUnmount() {
- this.editor.toTextArea();
- }
-
- onChange = () => {
- if (this.props.onChange) {
- this.props.onChange(this.editor.value());
- }
- };
-
- render() {
- return (
-
-
-
- );
- }
-}
-
-MarkdownEditor.propTypes = {
- onChange: PropTypes.func,
- value: PropTypes.string,
-};
+export default MarkdownEditor;
diff --git a/client/coral-framework/components/MarkdownEditor.css b/client/coral-framework/components/loadable/MarkdownEditor.css
similarity index 100%
rename from client/coral-framework/components/MarkdownEditor.css
rename to client/coral-framework/components/loadable/MarkdownEditor.css
diff --git a/client/coral-framework/components/loadable/MarkdownEditor.js b/client/coral-framework/components/loadable/MarkdownEditor.js
new file mode 100644
index 000000000..e9658ec69
--- /dev/null
+++ b/client/coral-framework/components/loadable/MarkdownEditor.js
@@ -0,0 +1,154 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import SimpleMDE from 'simplemde';
+import cn from 'classnames';
+import noop from 'lodash/noop';
+import styles from './MarkdownEditor.css';
+
+const config = {
+ status: false,
+
+ // Do not download fontAwesome icons as we replace them with
+ // material icons.
+ autoDownloadFontAwesome: false,
+
+ // Disable built-in spell checker as it is very rudimentary.
+ spellChecker: false,
+
+ toolbar: [
+ {
+ name: 'bold',
+ action: SimpleMDE.toggleBold,
+ className: styles.iconBold,
+ title: 'Bold',
+ },
+ {
+ name: 'italic',
+ action: SimpleMDE.toggleItalic,
+ className: styles.iconItalic,
+ title: 'Italic',
+ },
+ {
+ name: 'title',
+ action: SimpleMDE.toggleHeadingSmaller,
+ className: styles.iconTitle,
+ title: 'Title, Subtitle, Heading',
+ },
+ '|',
+ {
+ name: 'quote',
+ action: SimpleMDE.toggleBlockquote,
+ className: styles.iconQuote,
+ title: 'Quote',
+ },
+ {
+ name: 'unordered-list',
+ action: SimpleMDE.toggleUnorderedList,
+ className: styles.iconUnorderedList,
+ title: 'Generic List',
+ },
+ {
+ name: 'ordered-list',
+ action: SimpleMDE.toggleOrderedList,
+ className: styles.iconOrderedList,
+ title: 'Numbered List',
+ },
+ '|',
+ {
+ name: 'link',
+ action: SimpleMDE.drawLink,
+ className: styles.iconLink,
+ title: 'Create Link',
+ },
+ {
+ name: 'image',
+ action: SimpleMDE.drawImage,
+ className: styles.iconImage,
+ title: 'Insert Image',
+ },
+ '|',
+ {
+ name: 'preview',
+ action: SimpleMDE.togglePreview,
+ className: cn(styles.iconPreview, 'no-disable'),
+ title: 'Toggle Preview',
+ },
+ {
+ name: 'side-by-side',
+ action: SimpleMDE.toggleSideBySide,
+ className: cn(styles.iconSideBySide, 'no-disable'),
+ title: 'Toggle Side by Side',
+ },
+ {
+ name: 'fullscreen',
+ action: SimpleMDE.toggleFullScreen,
+ className: cn(styles.iconFullscreen, 'no-disable'),
+ title: 'Toggle Fullscreen',
+ },
+ '|',
+ {
+ name: 'guide',
+ action: 'https://simplemde.com/markdown-guide',
+ className: styles.iconGuide,
+ title: 'Markdown Guide',
+ },
+ ],
+};
+
+export default class MarkdownEditor extends Component {
+ textarea = null;
+ editor = null;
+
+ onRef = ref => (this.textarea = ref);
+
+ componentDidMount() {
+ this.editor = new SimpleMDE({
+ ...config,
+ element: this.textarea,
+ });
+
+ // Don't trap the key, to stay accessible.
+ this.editor.codemirror.options.extraKeys['Tab'] = false;
+ this.editor.codemirror.options.extraKeys['Shift-Tab'] = false;
+
+ this.editor.codemirror.on('change', this.onChange);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (
+ this.props.value !== nextProps.value &&
+ nextProps.value !== this.editor.value()
+ ) {
+ this.editor.value(nextProps.value);
+ }
+ }
+
+ componentDidUpdate() {
+ // Workaround empty render issue.
+ // https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
+ this.editor.codemirror.refresh();
+ }
+
+ componentWillUnmount() {
+ this.editor.toTextArea();
+ }
+
+ onChange = () => {
+ if (this.props.onChange) {
+ this.props.onChange(this.editor.value());
+ }
+ };
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+MarkdownEditor.propTypes = {
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+};
diff --git a/docs/.gitignore b/docs/.gitignore
index b9fd845b9..be0603043 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -2,4 +2,5 @@ public/*
!public/_redirects
.deploy*/
db.json
-*.log
\ No newline at end of file
+*.log
+source/_data/introspection.json
\ No newline at end of file
diff --git a/docs/source/api/graphql.md b/docs/source/api/graphql.md
index 792f2b240..4275fe7f0 100644
--- a/docs/source/api/graphql.md
+++ b/docs/source/api/graphql.md
@@ -12,4 +12,4 @@ interact with Talk's GraphQL endpoint.
# GraphQL Schema
-{% graphqldocs ../../client/coral-framework/graphql/introspection.json %}
\ No newline at end of file
+{% graphqldocs _data/introspection.json %}
\ No newline at end of file
diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js
index 01503cca0..0dc05a7be 100644
--- a/middleware/staticTemplate.js
+++ b/middleware/staticTemplate.js
@@ -66,28 +66,29 @@ function getManifest() {
}
/**
- * resolve is a function that can be used in templates to resolve an asset from
- * the manifest. In production, the manifest is cached.
+ * resolveFactory is a function that can be used in templates to resolve an
+ * asset from the manifest. In production, the manifest is cached.
*/
-const resolve = (() => {
+const createResolveFactory = (() => {
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]}`;
+ 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 => {
+ return () => {
+ let manifest = {};
try {
- const manifest = getManifest();
-
- return `${STATIC_URL}static/${manifest[key]}`;
+ manifest = getManifest();
} catch (err) {
console.warn(err);
- return '';
}
+
+ return key =>
+ key in manifest ? `${STATIC_URL}static/${manifest[key]}` : '';
};
})();
@@ -105,7 +106,7 @@ module.exports = async (req, res, next) => {
// Resolve will help resolving paths to static files
// using the manifest.
- res.locals.resolve = resolve;
+ res.locals.resolve = createResolveFactory();
// Forward the request.
next();
diff --git a/package.json b/package.json
index a807d3321..14116fc02 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"private": true,
"scripts": {
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js",
- "clean": "rm -rf dist client/coral-framework/graphql/introspection.json",
+ "clean": "rm -rf dist client/coral-framework/graphql/introspection.json docs/source/_data/introspection.json",
"watch": "npm-run-all clean generate-introspection --parallel watch:*",
"watch:client": "NODE_ENV=development webpack --progress --watch",
"watch:server": "nodemon --config .nodemon.json",
@@ -165,6 +165,7 @@
"react-broadcast": "^0.6.2",
"react-dom": "^15.4.2",
"react-input-autosize": "^1.1.4",
+ "react-loadable": "^5.3.1",
"react-mdl": "^1.11.0",
"react-mdl-selectfield": "^0.2.0",
"react-paginate": "^5.0.0",
diff --git a/scripts/generateIntrospectionResult.js b/scripts/generateIntrospectionResult.js
index e07fe7c6d..7c3ea024c 100755
--- a/scripts/generateIntrospectionResult.js
+++ b/scripts/generateIntrospectionResult.js
@@ -1,7 +1,120 @@
#! /usr/bin/env node
const path = require('path');
-const introspectionFilename = path.resolve(
+const fs = require('fs');
+const { graphql } = require('graphql');
+const schema = require('../graph/schema');
+
+// Copied from https://github.com/graphql/graphql-js/blob/f995c1f92e94d9c451104b6a0db8034165ef8640/src/utilities/introspectionQuery.js#L18-L113
+// which is available in graphql@0.13.2
+//
+// TODO: remove when we upgrade to at least graphql@0.13.2.
+function getIntrospectionQuery(options = {}) {
+ const descriptions = !(options && options.descriptions === false);
+ return `
+ query IntrospectionQuery {
+ __schema {
+ queryType { name }
+ mutationType { name }
+ subscriptionType { name }
+ types {
+ ...FullType
+ }
+ directives {
+ name
+ ${descriptions ? 'description' : ''}
+ locations
+ args {
+ ...InputValue
+ }
+ }
+ }
+ }
+ fragment FullType on __Type {
+ kind
+ name
+ ${descriptions ? 'description' : ''}
+ fields(includeDeprecated: true) {
+ name
+ ${descriptions ? 'description' : ''}
+ args {
+ ...InputValue
+ }
+ type {
+ ...TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ...InputValue
+ }
+ interfaces {
+ ...TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ ${descriptions ? 'description' : ''}
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ...TypeRef
+ }
+ }
+ fragment InputValue on __InputValue {
+ name
+ ${descriptions ? 'description' : ''}
+ type { ...TypeRef }
+ defaultValue
+ }
+ fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+}
+
+const generateIntrospectionResult = (resultLocation, options = {}) =>
+ graphql(schema, getIntrospectionQuery(options)).then(({ data }) => {
+ // Serialize the introspection result as JSON.
+ const introspectionResult = JSON.stringify(data, null, 2);
+
+ // Write the introspection result to the filesystem.
+ fs.writeFileSync(resultLocation, introspectionResult, 'utf8');
+
+ console.log(`Outputted result of introspectionQuery to ${resultLocation}`);
+ });
+
+const graphIntrospectionFilename = path.resolve(
__dirname,
'..',
'client',
@@ -10,23 +123,21 @@ const introspectionFilename = path.resolve(
'introspection.json'
);
-const fs = require('fs');
-const { graphql, introspectionQuery } = require('graphql');
-const schema = require('../graph/schema');
+const docsIntrospectionFilename = path.resolve(
+ __dirname,
+ '..',
+ 'docs',
+ 'source',
+ '_data',
+ 'introspection.json'
+);
-graphql(schema, introspectionQuery)
- .then(({ data }) => {
- // Serialize the introspection result as JSON.
- const introspectionResult = JSON.stringify(data, null, 2);
-
- // Write the introspection result to the filesystem.
- fs.writeFileSync(introspectionFilename, introspectionResult, 'utf8');
-
- console.log(
- `Outputted result of introspectionQuery to ${introspectionFilename}`
- );
- })
- .catch(err => {
- console.error(err);
- process.exit(1);
- });
+Promise.all([
+ generateIntrospectionResult(graphIntrospectionFilename, {
+ descriptions: false,
+ }),
+ generateIntrospectionResult(docsIntrospectionFilename),
+]).catch(err => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/webpack.config.js b/webpack.config.js
index 184107491..d6f216d02 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -335,7 +335,8 @@ module.exports = [
{
output: {
library: 'Coral',
- // don't hash the embed.
+ // don't hash the embed, cache-busting must be completed by the requester
+ // as this lives in a static template on the embed site.
filename: '[name].js',
},
plugins: [
diff --git a/yarn.lock b/yarn.lock
index 342529006..fd501351e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9217,6 +9217,12 @@ react-linkify@^0.2.1:
prop-types "^15.5.8"
tlds "^1.57.0"
+react-loadable@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/react-loadable/-/react-loadable-5.3.1.tgz#9699e9a08fed49bacd69caaa282034b62a76bcdd"
+ dependencies:
+ prop-types "^15.5.0"
+
react-mdl-selectfield@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/react-mdl-selectfield/-/react-mdl-selectfield-0.2.0.tgz#36e1a97233036c057ab2bdb31ec09ad8d9988411"