From 3ec7bf09bf25e102ce5037d2cc4805b609c12500 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:28:44 -0600 Subject: [PATCH 1/5] improved introspection --- .nodemon.json | 2 +- docs/.gitignore | 3 +- docs/source/api/graphql.md | 2 +- scripts/generateIntrospectionResult.js | 151 +++++++++++++++++++++---- 4 files changed, 135 insertions(+), 23 deletions(-) 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/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/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); +}); From 4f87c88e0401058f1ef74a51f786af85a7fdcf10 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:29:07 -0600 Subject: [PATCH 2/5] improved static template resolve in development --- middleware/staticTemplate.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) 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(); From 59982d4329cfe13d90453363a1238322de31ec9f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Mar 2018 17:29:38 -0600 Subject: [PATCH 3/5] swapped manual dynamic chunked loader with readt-loadable --- .../components/QuestionBoxBuilder.js | 90 ++----------------- .../{ => loadable}/QuestionBoxBuilder.css | 0 .../components/loadable/QuestionBoxBuilder.js | 56 ++++++++++++ package.json | 1 + webpack.config.js | 3 +- yarn.lock | 6 ++ 6 files changed, 73 insertions(+), 83 deletions(-) rename client/coral-embed-stream/src/tabs/configure/components/{ => loadable}/QuestionBoxBuilder.css (100%) create mode 100644 client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js 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..e87e7b321 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js +++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js @@ -1,85 +1,11 @@ -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 { Spinner } from 'coral-ui'; +import Loadable from 'react-loadable'; -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, - questionBoxContent, - onContentChange, - onIconChange, - } = this.props; - const { loading, MarkdownEditor } = this.state; - - if (loading) { - return ; - } - - return ( -
-

Include an Icon

- -
    - {icons.map(item => { - const name = typeof item === 'object' ? Object.keys(item)[0] : item; - const icon = typeof item === 'object' ? item[name] : item; - return ( -
  • - -
  • - ); - })} -
- - - - -
- ); - } -} +const QuestionBoxBuilder = Loadable({ + loader: () => + import(/* webpackChunkName: "questionBoxBuilder" */ + './loadable/QuestionBoxBuilder'), + loading: Spinner, +}); export default QuestionBoxBuilder; diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css similarity index 100% rename from client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css rename to client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js new file mode 100644 index 000000000..837de1280 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js @@ -0,0 +1,56 @@ +import React from 'react'; +import QuestionBox from '../../../../components/QuestionBox'; +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 { + render() { + const { + questionBoxIcon, + questionBoxContent, + onContentChange, + onIconChange, + } = this.props; + + return ( +
+

Include an Icon

+ +
    + {icons.map(item => { + const name = typeof item === 'object' ? Object.keys(item)[0] : item; + const icon = typeof item === 'object' ? item[name] : item; + return ( +
  • + +
  • + ); + })} +
+ + + + +
+ ); + } +} + +export default QuestionBoxBuilder; diff --git a/package.json b/package.json index a807d3321..a3d5f665e 100644 --- a/package.json +++ b/package.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/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" From d7cf733a4298a885b20c936ecdc115d4a9f9f845 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 23 Mar 2018 11:15:47 -0600 Subject: [PATCH 4/5] Moved loadable to the markdown editor --- .../{loadable => }/QuestionBoxBuilder.css | 0 .../components/QuestionBoxBuilder.js | 61 ++++++- .../components/loadable/QuestionBoxBuilder.js | 56 ------ .../components/MarkdownEditor.js | 161 +----------------- .../{ => loadable}/MarkdownEditor.css | 0 .../components/loadable/MarkdownEditor.js | 154 +++++++++++++++++ 6 files changed, 216 insertions(+), 216 deletions(-) rename client/coral-embed-stream/src/tabs/configure/components/{loadable => }/QuestionBoxBuilder.css (100%) delete mode 100644 client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js rename client/coral-framework/components/{ => loadable}/MarkdownEditor.css (100%) create mode 100644 client/coral-framework/components/loadable/MarkdownEditor.js diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css similarity index 100% rename from client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.css rename to client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css 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 e87e7b321..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,11 +1,56 @@ -import { Spinner } from 'coral-ui'; -import Loadable from 'react-loadable'; +import React from 'react'; +import QuestionBox from '../../../components/QuestionBox'; +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 QuestionBoxBuilder = Loadable({ - loader: () => - import(/* webpackChunkName: "questionBoxBuilder" */ - './loadable/QuestionBoxBuilder'), - loading: Spinner, -}); +const DefaultIcon = ; +const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote']; + +class QuestionBoxBuilder extends React.Component { + render() { + const { + questionBoxIcon, + questionBoxContent, + onContentChange, + onIconChange, + } = this.props; + + return ( +
+

Include an Icon

+ +
    + {icons.map(item => { + const name = typeof item === 'object' ? Object.keys(item)[0] : item; + const icon = typeof item === 'object' ? item[name] : item; + return ( +
  • + +
  • + ); + })} +
+ + + + +
+ ); + } +} export default QuestionBoxBuilder; diff --git a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js deleted file mode 100644 index 837de1280..000000000 --- a/client/coral-embed-stream/src/tabs/configure/components/loadable/QuestionBoxBuilder.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import QuestionBox from '../../../../components/QuestionBox'; -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 { - render() { - const { - questionBoxIcon, - questionBoxContent, - onContentChange, - onIconChange, - } = this.props; - - return ( -
-

Include an Icon

- -
    - {icons.map(item => { - const name = typeof item === 'object' ? Object.keys(item)[0] : item; - const icon = typeof item === 'object' ? item[name] : item; - return ( -
  • - -
  • - ); - })} -
- - - - -
- ); - } -} - -export default QuestionBoxBuilder; 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 ( -
-