mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 01:04:09 +08:00
Merge branch 'master' into party-bugsa
This commit is contained in:
+1
-1
@@ -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": [
|
||||
".",
|
||||
|
||||
@@ -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 = <DefaultQuestionBoxIcon className={styles.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 <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...this.props} onChange={noop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
export default MarkdownEditor;
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...this.props} onChange={noop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
+2
-1
@@ -2,4 +2,5 @@ public/*
|
||||
!public/_redirects
|
||||
.deploy*/
|
||||
db.json
|
||||
*.log
|
||||
*.log
|
||||
source/_data/introspection.json
|
||||
@@ -12,4 +12,4 @@ interact with Talk's GraphQL endpoint.
|
||||
|
||||
# GraphQL Schema
|
||||
|
||||
{% graphqldocs ../../client/coral-framework/graphql/introspection.json %}
|
||||
{% graphqldocs _data/introspection.json %}
|
||||
@@ -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();
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
+2
-1
@@ -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: [
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user