From 2ec5b243fbd57173ec64cb4444c4ea02bff9d9b1 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Mon, 27 Mar 2017 12:43:03 -0600 Subject: [PATCH 01/16] hot-load email templates during development --- ...ail-confirm.ejs => email-confirm.html.ejs} | 0 ...notification.ejs => notification.html.ejs} | 0 ...word-reset.ejs => password-reset.html.ejs} | 0 services/mailer.js | 46 ++++++++++++++----- 4 files changed, 35 insertions(+), 11 deletions(-) rename services/email/{email-confirm.ejs => email-confirm.html.ejs} (100%) rename services/email/{notification.ejs => notification.html.ejs} (100%) rename services/email/{password-reset.ejs => password-reset.html.ejs} (100%) diff --git a/services/email/email-confirm.ejs b/services/email/email-confirm.html.ejs similarity index 100% rename from services/email/email-confirm.ejs rename to services/email/email-confirm.html.ejs diff --git a/services/email/notification.ejs b/services/email/notification.html.ejs similarity index 100% rename from services/email/notification.ejs rename to services/email/notification.html.ejs diff --git a/services/email/password-reset.ejs b/services/email/password-reset.html.ejs similarity index 100% rename from services/email/password-reset.ejs rename to services/email/password-reset.html.ejs diff --git a/services/mailer.js b/services/mailer.js index 2928570ad..fe909079f 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -17,18 +17,42 @@ if (smtpRequiredProps.some(prop => !process.env[prop])) { } // load all the templates as strings -const templateStrings = {}; -fs.readdir(path.join(__dirname, 'email'), (err, files) => { - if (err) { - throw err; - } +const templates = {}; - files.forEach(file => { - fs.readFile(path.join(__dirname, 'email', file), 'utf8', (err, data) => { - templateStrings[file] = _.template(data); +// load the temlates per request during development +if (process.env.NODE_ENV !== 'production') { + templates.render = (name, format = 'txt', context) => new Promise((resolve, reject) => { + fs.readFile(path.join(__dirname, 'email', [name, format, 'ejs'].join('.')), (err, file) => { + if (err) { + return reject(err); + } + + return resolve(_.template(file)(context)); }); }); -}); +} else { + templates.data = fs.readdirSync(path.join(__dirname, 'email')).reduce((data, filename) => { + + // split the filename + let splitFileName = filename.split('.'); + + // remove the ejs extension + splitFileName.splice(-1, 1); + + const [name, format] = splitFileName; + + if (!(name in data)) { + data[name] = {}; + } + + data[name][format] = _.template(fs.readFileSync(filename, 'utf8')); + + return data; + }); + + // render a template based on the request name + templates.render = (name, format = 'txt', context) => templates.data[name][format](context); +} const options = { host: process.env.TALK_SMTP_HOST, @@ -70,10 +94,10 @@ const mailer = module.exports = { return Promise.all([ // Render the HTML version of the email. - templateStrings[`${template}.ejs`](locals), + templates.render(template, 'html', locals), // Render the TEXT version of the email. - templateStrings[`${template}.txt.ejs`](locals) + templates.render(template, 'txt', locals) ]) .then(([html, text]) => { From e7c0c5b63871d1ac84583fb35d417dea885c2901 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 28 Mar 2017 09:54:27 -0600 Subject: [PATCH 02/16] adjusted path --- services/mailer.js | 53 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/services/mailer.js b/services/mailer.js index fe909079f..35741ee9f 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -17,42 +17,43 @@ if (smtpRequiredProps.some(prop => !process.env[prop])) { } // load all the templates as strings -const templates = {}; +const templates = { + data: {} +}; // load the temlates per request during development -if (process.env.NODE_ENV !== 'production') { - templates.render = (name, format = 'txt', context) => new Promise((resolve, reject) => { - fs.readFile(path.join(__dirname, 'email', [name, format, 'ejs'].join('.')), (err, file) => { - if (err) { - return reject(err); - } +templates.render = (name, format = 'txt', context) => new Promise((resolve, reject) => { - return resolve(_.template(file)(context)); - }); - }); -} else { - templates.data = fs.readdirSync(path.join(__dirname, 'email')).reduce((data, filename) => { + // If we are in production mode, check the view cache. + if (process.env.NODE_ENV === 'production') { + if (name in templates.data && format in templates.data[name]) { + let view = templates.data[name][format]; - // split the filename - let splitFileName = filename.split('.'); + return resolve(view(context)); + } + } - // remove the ejs extension - splitFileName.splice(-1, 1); + const filename = path.join(__dirname, 'email', [name, format, 'ejs'].join('.')); - const [name, format] = splitFileName; - - if (!(name in data)) { - data[name] = {}; + fs.readFile(filename, (err, file) => { + if (err) { + return reject(err); } - data[name][format] = _.template(fs.readFileSync(filename, 'utf8')); + let view = _.template(file); - return data; + // If we are in production mode, fill the view cache. + if (process.env.NODE_ENV === 'production') { + if (!(name in templates.data)) { + templates.data[name] = {}; + } + + templates.data[name][format] = view; + } + + return resolve(view(context)); }); - - // render a template based on the request name - templates.render = (name, format = 'txt', context) => templates.data[name][format](context); -} +}); const options = { host: process.env.TALK_SMTP_HOST, From 4e60689fa415b17d20ca8be6e791de8268cb0446 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 28 Mar 2017 14:31:43 -0600 Subject: [PATCH 03/16] view a permalink thread in isolation --- client/coral-embed-stream/src/Embed.js | 95 +++++++++++-------- client/coral-embed/src/index.js | 9 ++ client/coral-framework/actions/asset.js | 35 +++++++ client/coral-framework/constants/asset.js | 2 + .../coral-framework/graphql/queries/index.js | 3 + 5 files changed, 104 insertions(+), 40 deletions(-) diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index a3d2a24a9..61e5ab1b9 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; const lang = new I18n(translations); -import {TabBar, Tab, TabContent, Spinner} from 'coral-ui'; +import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui'; const {logout, showSignInDialog, requestConfirmEmail} = authActions; const {addNotification, clearNotification} = notificationActions; @@ -15,7 +15,7 @@ const {fetchAssetSuccess} = assetActions; import {queryStream} from 'coral-framework/graphql/queries'; import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations'; import {editName} from 'coral-framework/actions/user'; -import {updateCountCache} from 'coral-framework/actions/asset'; +import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; import Stream from './Stream'; @@ -79,7 +79,7 @@ class Embed extends Component { if(!isEqual(prevProps.data.comment, this.props.data.comment)) { // Scroll to a permalinked comment if one is in the URL once the page is done rendering. - setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0); + setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0); } } @@ -124,6 +124,16 @@ class Embed extends Component { {lang.t('MY_COMMENTS')} Configure Stream + { + highlightedComment && + + } {loggedIn && this.props.logout().then(refetch)} changeTab={this.changeTab}/>} { @@ -170,9 +180,11 @@ class Embed extends Component { offset={signInOffset}/>} {loggedIn && user && } {loggedIn && } + + {/* the highlightedComment is isolated after the user followed a permalink */} { - highlightedComment && - + :
+ +
+ +
+ asset.comments.length} + loadMore={this.props.loadMore} /> +
} - -
- -
- asset.comments.length} - loadMore={this.props.loadMore}/>
({ editName: (username) => dispatch(editName(username)), showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), updateCountCache: (id, count) => dispatch(updateCountCache(id, count)), + viewAllComments: () => dispatch(viewAllComments()), logout: () => dispatch(logout()), dispatch: d => dispatch(d) }); diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 8ebd403b8..e3350e0d3 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -74,6 +74,15 @@ function configurePymParent(pymParent, asset_url) { snackbar.style.opacity = 0; }); + // remove the permalink comment id from the hash + pymParent.onMessage('coral-view-all-comments', function () { + window.history.replaceState( + {}, + document.title, + location.origin + location.pathname + location.search + ); + }); + pymParent.onMessage('coral-alert', function (message) { const [type, text] = message.split('|'); snackbar.style.transform = 'translate(-50%, 20px)'; diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js index 609525986..fa927e58a 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-framework/actions/asset.js @@ -1,6 +1,7 @@ import * as actions from '../constants/asset'; import coralApi from '../helpers/response'; import {addNotification} from '../actions/notification'; +import {pym} from 'coral-framework'; import I18n from '../../coral-framework/modules/i18n/i18n'; import translations from './../translations'; @@ -49,3 +50,37 @@ export const updateOpenStatus = status => dispatch => { dispatch(updateOpenStream({closedAt: new Date().getTime()})); } }; + +function removeParam(key, sourceURL) { + let rtn = sourceURL.split('?')[0]; + let param; + let params_arr = []; + let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : ''; + if (queryString !== '') { + params_arr = queryString.split('&'); + for (let i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split('=')[0]; + if (param === key) { + params_arr.splice(i, 1); + } + } + rtn = `${rtn}?${params_arr.join('&')}`; + } + return rtn; +} + +export const viewAllComments = () => { + + // remove the comment_id url param + const modifiedUrl = removeParam('comment_id', location.href); + try { + + // "window" here refers to the embedded iframe + window.history.replaceState({}, document.title, modifiedUrl); + + // also change the parent url + pym.sendMessage('coral-view-all-comments'); + } catch (e) { /* not sure if we're worried about old browsers */ } + + return {type: actions.VIEW_ALL_COMMENTS}; +}; diff --git a/client/coral-framework/constants/asset.js b/client/coral-framework/constants/asset.js index 234095d9d..5547c5284 100644 --- a/client/coral-framework/constants/asset.js +++ b/client/coral-framework/constants/asset.js @@ -9,3 +9,5 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE'; export const OPEN_COMMENTS = 'OPEN_COMMENTS'; export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE'; + +export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS'; diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index cdf2356d8..a6c8aa17b 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -20,6 +20,7 @@ function getQueryVariable(variable) { return null; } +// get the counts of the top-level comments export const getCounts = (data) => ({asset_id, limit, sort}) => { return data.fetchMore({ query: GET_COUNTS, @@ -41,6 +42,7 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => { }); }; +// handle paginated requests for more Comments pertaining to the Asset export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => { return data.fetchMore({ query: LOAD_MORE, @@ -94,6 +96,7 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s }); }; +// load the comment stream. export const queryStream = graphql(STREAM_QUERY, { options: () => { let comment_id = getQueryVariable('comment_id'); From b7ce669cfc9fe178092fc8182ba7b174be16bc6c Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 28 Mar 2017 14:46:01 -0600 Subject: [PATCH 04/16] add translations --- client/coral-embed-stream/src/Embed.js | 2 +- client/coral-framework/translations.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 61e5ab1b9..918d8a4c7 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -132,7 +132,7 @@ class Embed extends Component { onClick={() => { this.props.viewAllComments(); this.props.data.refetch(); - }}>Show all comments + }}>{lang.t('showAllComments')} } {loggedIn && this.props.logout().then(refetch)} changeTab={this.changeTab}/>} diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index f9038aba7..31c3e98ea 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -13,6 +13,7 @@ "error": "Usernames can contain letters, numbers and _ only" }, "viewMoreComments": "view more comments", + "showAllComments": "Show all comments", "viewReply": "view reply", "viewAllRepliesInitial": "view all {0} replies", "viewAllReplies": "view {0} replies", @@ -56,6 +57,7 @@ "newCount": "Ver {0} {1} más", "comment": "commentario", "comments": "commentarios", + "showAllComments": "Mostrar todos los comentarios", "error": { "emailNotVerified": "E-mail {0} no verificado.", "email": "No es un e-mail válido", From f11a11cf675239914224d82fd3d4e4022a6dac64 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 29 Mar 2017 13:04:28 -0600 Subject: [PATCH 05/16] show view more button on a permalinked comment if applicable --- client/coral-framework/graphql/queries/streamQuery.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 03af1de4c..f3b8576dd 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -5,6 +5,7 @@ query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comm ...commentView parent { ...commentView + replyCount replies { ...commentView } From 3007ba68f8709cf2f6482242bdf9d1f143de2c4a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 29 Mar 2017 15:12:33 -0600 Subject: [PATCH 06/16] load more replies on a permalinked comment --- .../coral-framework/graphql/queries/index.js | 44 +++++++++++++++---- .../graphql/queries/streamQuery.graphql | 7 +++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index a6c8aa17b..ec0dcc336 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -5,6 +5,7 @@ import GET_COUNTS from './getCounts.graphql'; import MY_COMMENT_HISTORY from './myCommentHistory.graphql'; import uniqBy from 'lodash/uniqBy'; import sortBy from 'lodash/sortBy'; +import isNil from 'lodash/isNil'; function getQueryVariable(variable) { let query = window.location.search.substring(1); @@ -47,19 +48,43 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s return data.fetchMore({ query: LOAD_MORE, variables: { - limit, - cursor, - parent_id, - asset_id, - sort + limit, // how many comments are we returning + cursor, // the date of the first/last comment depending on the sort order + parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment + asset_id, // the id of the asset we're currently on + sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL }, updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => { let updatedAsset; - if (parent_id) { + if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment + + let comment = {}; + if (oldData.comment && oldData.comment.parent) { + + // put comments (replies) onto the oldData.comment.parent object + // the initial comment permalinked was a reply + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id'); + comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')}; + } else if (oldData.comment) { + + // put the comments (replies) directly onto oldData.comment + // the initial comment permalinked was a top-level comment + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id'); + comment.replies = sortBy(uniqReplies, 'created_at'); + } + + updatedAsset = { + ...oldData, + comment: { + ...oldData.comment, + ...comment + } + }; + + } else if (parent_id) { // If loading more replies - // If loading more replies updatedAsset = { ...oldData, asset: { @@ -78,9 +103,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s }) } }; - } else { + } else { // If loading more top-level comments - // If loading more top-level comments updatedAsset = { ...oldData, asset: { @@ -99,6 +123,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s // load the comment stream. export const queryStream = graphql(STREAM_QUERY, { options: () => { + + // where the query string is from the embeded iframe url let comment_id = getQueryVariable('comment_id'); let has_comment = comment_id != null; diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index f3b8576dd..c9f9d3d5b 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -1,8 +1,15 @@ #import "../fragments/commentView.graphql" query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) { + # the comment here is for loading one comment and it's children, probably after following a permalink + # $has_comment is derived from the comment_id query param in the iframe url, + # which is in turn pulled from the host page url comment(id: $comment_id) @include(if: $has_comment) { ...commentView + replyCount + replies { + ...commentView + } parent { ...commentView replyCount From f37c76866292787b25b33d3d470e6f374e64c643 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 29 Mar 2017 15:21:41 -0600 Subject: [PATCH 07/16] only highlight the actual comment permalinked --- client/coral-embed-stream/src/Comment.js | 49 ++++++++++++------------ client/coral-embed-stream/src/Embed.js | 2 + 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index caffb6894..d970a5f35 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -117,7 +117,6 @@ class Comment extends React.Component { const flag = getActionSummary('FlagActionSummary', comment); const dontagree = getActionSummary('DontAgreeActionSummary', comment); let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; - commentClass += highlighted === comment.id ? ' highlighted-comment' : ''; commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) @@ -147,18 +146,19 @@ class Comment extends React.Component { id={`c_${comment.id}`} style={{marginLeft: depth * 30}}>
- - { isStaff(comment.tags) - ? Staff +
+ + { isStaff(comment.tags) + ? Staff : null } - { commentIsBest(comment) - ? + { commentIsBest(comment) + ? : null } - + - +
-
- - - - - - +
+ + + + + + +
{ activeReplyBox === comment.id diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 5818da2dd..12d6733d1 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -97,6 +97,8 @@ class Embed extends Component { const {closedAt, countCache = {}} = this.props.asset; const {loading, asset, refetch, comment} = this.props.data; const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth; + + // even though the permalinked comment is the highlighted one, we're displaying its parent + replies const highlightedComment = comment && comment.parent ? comment.parent : comment; const openStream = closedAt === null; From e878d1ebfd2082670c25686680aba67a0d535d04 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 30 Mar 2017 17:13:02 +0700 Subject: [PATCH 08/16] Upgrade React --- package.json | 6 +- yarn.lock | 297 ++++++++++----------------------------------------- 2 files changed, 62 insertions(+), 241 deletions(-) diff --git a/package.json b/package.json index 6fdf4560a..41598341f 100644 --- a/package.json +++ b/package.json @@ -148,9 +148,9 @@ "pre-git": "^3.10.0", "precss": "^1.4.0", "pym.js": "^1.1.1", - "react": "15.3.2", - "react-addons-test-utils": "15.3.2", - "react-dom": "15.3.2", + "react": "^15.4.2", + "react-addons-test-utils": "^15.4.2", + "react-dom": "^15.4.2", "react-highlight-words": "^0.6.0", "react-linkify": "^0.1.3", "react-mdl": "^1.7.2", diff --git a/yarn.lock b/yarn.lock index 81ddf18b4..82a0f906d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,19 +397,7 @@ babel-eslint@^7.2.1: babel-types "^6.23.0" babylon "^6.16.1" -babel-generator@^6.18.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" - dependencies: - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - -babel-generator@^6.24.0: +babel-generator@^6.18.0, babel-generator@^6.24.0: version "6.24.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.0.tgz#eba270a8cc4ce6e09a61be43465d7c62c1f87c56" dependencies: @@ -482,17 +470,7 @@ babel-helper-explode-class@^6.22.0: babel-traverse "^6.22.0" babel-types "^6.22.0" -babel-helper-function-name@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.22.0.tgz#51f1bdc4bb89b15f57a9b249f33d742816dcbefc" - dependencies: - babel-helper-get-function-arity "^6.22.0" - babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" - -babel-helper-function-name@^6.23.0: +babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz#25742d67175c8903dbe4b6cb9d9e1fcb8dcf23a6" dependencies: @@ -576,13 +554,7 @@ babel-loader@^6.4.1: mkdirp "^0.5.1" object-assign "^4.0.1" -babel-messages@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.22.0.tgz#36066a214f1217e4ed4164867669ecb39e3ea575" - dependencies: - babel-runtime "^6.22.0" - -babel-messages@^6.23.0: +babel-messages@^6.22.0, babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" dependencies: @@ -686,16 +658,7 @@ babel-plugin-transform-class-constructor-call@^6.22.0: babel-runtime "^6.22.0" babel-template "^6.22.0" -babel-plugin-transform-class-properties@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.22.0.tgz#aa78f8134495c7de06c097118ba061844e1dc1d8" - dependencies: - babel-helper-function-name "^6.22.0" - babel-plugin-syntax-class-properties "^6.8.0" - babel-runtime "^6.22.0" - babel-template "^6.22.0" - -babel-plugin-transform-class-properties@^6.23.0: +babel-plugin-transform-class-properties@^6.22.0, babel-plugin-transform-class-properties@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.23.0.tgz#187b747ee404399013563c993db038f34754ac3b" dependencies: @@ -925,14 +888,7 @@ babel-plugin-transform-object-assign@^6.8.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-object-rest-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.22.0.tgz#1d419b55e68d2e4f64a5ff3373bd67d73c8e83bc" - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-object-rest-spread@^6.23.0: +babel-plugin-transform-object-rest-spread@^6.22.0, babel-plugin-transform-object-rest-spread@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" dependencies: @@ -1057,17 +1013,7 @@ babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtim core-js "^2.4.0" regenerator-runtime "^0.10.0" -babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.3.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.22.0.tgz#403d110905a4626b317a2a1fcb8f3b73204b2edb" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" - babylon "^6.11.0" - lodash "^4.2.0" - -babel-template@^6.23.0: +babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0, babel-template@^6.3.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638" dependencies: @@ -1077,21 +1023,7 @@ babel-template@^6.23.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.18.0, babel-traverse@^6.22.0: - version "6.22.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.22.1.tgz#3b95cd6b7427d6f1f757704908f2fc9748a5f59f" - dependencies: - babel-code-frame "^6.22.0" - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-traverse@^6.23.0, babel-traverse@^6.23.1: +babel-traverse@^6.18.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.23.1: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" dependencies: @@ -1105,16 +1037,7 @@ babel-traverse@^6.23.0, babel-traverse@^6.23.1: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.22.0.tgz#2a447e8d0ea25d2512409e4175479fd78cc8b1db" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.23.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" dependencies: @@ -1123,11 +1046,11 @@ babel-types@^6.23.0: lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: +babylon@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" -babylon@^6.16.1: +babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1: version "6.16.1" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" @@ -1221,22 +1144,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.2: - version "1.16.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "2.6.0" - depd "~1.1.0" - http-errors "~1.5.1" - iconv-lite "0.4.15" - on-finished "~2.3.0" - qs "6.2.1" - raw-body "~2.2.0" - type-is "~1.6.14" - -body-parser@^1.17.1: +body-parser@^1.12.2, body-parser@^1.17.1: version "1.17.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" dependencies: @@ -2186,9 +2094,9 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.0, debug@^2.1.1, debug@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" +debug@*, debug@2, debug@2.6.3, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" dependencies: ms "0.7.2" @@ -2210,12 +2118,6 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.3, debug@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - debug@^0.7.2, debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2797,10 +2699,6 @@ esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -etag@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" - etag@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" @@ -2871,38 +2769,7 @@ express-session@^1.15.1: uid-safe "~2.1.3" utils-merge "1.0.0" -express@^4.12.2: - version "4.14.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" - dependencies: - accepts "~1.3.3" - array-flatten "1.1.1" - content-disposition "0.5.2" - content-type "~1.0.2" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "~2.2.0" - depd "~1.1.0" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - finalhandler "0.5.1" - fresh "0.3.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.1" - path-to-regexp "0.1.7" - proxy-addr "~1.1.3" - qs "6.2.0" - range-parser "~1.2.0" - send "0.14.2" - serve-static "~1.11.2" - type-is "~1.6.14" - utils-merge "1.0.0" - vary "~1.1.0" - -express@^4.15.2: +express@^4.12.2, express@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" dependencies: @@ -2967,7 +2834,7 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" -fbjs@^0.8.4: +fbjs@^0.8.1, fbjs@^0.8.4: version "0.8.8" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6" dependencies: @@ -3038,16 +2905,6 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd" - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.0.tgz#b5691c2c0912092f18ac23e9416bde5cd7dc6755" @@ -3167,10 +3024,6 @@ frameguard@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.0.0.tgz#7bcad469ee7b96e91d12ceb3959c78235a9272e9" -fresh@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" - fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" @@ -3756,7 +3609,7 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" -http-errors@~1.5.0, http-errors@~1.5.1: +http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" dependencies: @@ -4269,19 +4122,7 @@ istanbul-lib-hook@^1.0.0: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.3.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e" - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.13.0" - istanbul-lib-coverage "^1.0.0" - semver "^5.3.0" - -istanbul-lib-instrument@^1.6.2: +istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.6.2.tgz#dac644f358f51efd6113536d7070959a0111f73b" dependencies: @@ -6280,18 +6121,18 @@ postcss@5.1.2: source-map "^0.5.6" supports-color "^3.1.2" -postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.11, postcss@^5.2.4, postcss@^5.2.5: - version "5.2.11" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.11.tgz#ff29bcd6d2efb98bfe08a022055ec599bbe7b761" +postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.15, postcss@^5.2.4, postcss@^5.2.5: + version "5.2.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" source-map "^0.5.6" supports-color "^3.2.3" -postcss@^5.2.15: - version "5.2.16" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57" +postcss@^5.0.6, postcss@^5.2.11: + version "5.2.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.11.tgz#ff29bcd6d2efb98bfe08a022055ec599bbe7b761" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" @@ -6543,15 +6384,7 @@ q@^1.1.2: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -qs@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" - -qs@6.2.1, qs@^6.1.0, qs@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" - -qs@6.4.0: +qs@6.4.0, qs@^6.1.0, qs@^6.2.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -6622,9 +6455,12 @@ rc@^1.0.1, rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -react-addons-test-utils@15.3.2: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6" +react-addons-test-utils@^15.4.2: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz#93bcaa718fcae7360d42e8fb1c09756cc36302a2" + dependencies: + fbjs "^0.8.4" + object-assign "^4.1.0" react-apollo@^0.10.0: version "0.10.1" @@ -6641,10 +6477,18 @@ react-apollo@^0.10.0: optionalDependencies: react-dom "0.14.x || 15.* || ^15.0.0" -"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@15.3.2, react-dom@^15.3.1: +"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@^15.3.1: version "15.3.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" +react-dom@^15.4.2: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f" + dependencies: + fbjs "^0.8.1" + loose-envify "^1.1.0" + object-assign "^4.1.0" + react-highlight-words@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.6.0.tgz#e12e9fedda4333e410ea408cdedffc77122020aa" @@ -6707,7 +6551,7 @@ react-tagsinput@^3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.14.0.tgz#6729f8d5b313ed8fbb35496205ec2a97654af6bc" -react@15.3.2, react@^15.3.1: +react@^15.3.1: version "15.3.2" resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e" dependencies: @@ -6715,6 +6559,14 @@ react@15.3.2, react@^15.3.1: loose-envify "^1.1.0" object-assign "^4.1.0" +react@^15.4.2: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" + dependencies: + fbjs "^0.8.4" + loose-envify "^1.1.0" + object-assign "^4.1.0" + read-all-stream@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" @@ -6828,11 +6680,7 @@ redis-commands@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" -redis-parser@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.4.0.tgz#018ea743077aae944d0b798b2fd12587320bf3c9" - -redis-parser@^2.5.0: +redis-parser@^2.0.0, redis-parser@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.5.0.tgz#79fc2b1d4a6e4d2870b35368433639271fca2617" @@ -6840,15 +6688,7 @@ redis@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e" -redis@^2.1.0, redis@~2.6.0-2: - version "2.6.5" - resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687" - dependencies: - double-ended-queue "^2.1.0-0" - redis-commands "^1.2.0" - redis-parser "^2.0.0" - -redis@^2.7.1: +redis@^2.1.0, redis@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/redis/-/redis-2.7.1.tgz#7d56f7875b98b20410b71539f1d878ed58ebf46a" dependencies: @@ -6856,6 +6696,14 @@ redis@^2.7.1: redis-commands "^1.2.0" redis-parser "^2.5.0" +redis@~2.6.0-2: + version "2.6.5" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687" + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.0.0" + reds@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/reds/-/reds-0.2.5.tgz#38a767f7663cd749036848697d82c74fd29bc01f" @@ -7172,24 +7020,6 @@ semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" -send@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" - dependencies: - debug "~2.2.0" - depd "~1.1.0" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.5.1" - mime "1.3.4" - ms "0.7.2" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - send@0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" @@ -7217,15 +7047,6 @@ serve-static@1.12.1: parseurl "~1.3.1" send "0.15.1" -serve-static@~1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.14.2" - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" From 2d0208046be2343351d493de17d7cb2c7d9f15a7 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 30 Mar 2017 17:38:41 +0700 Subject: [PATCH 09/16] Implement MarkdownEditor --- .../src/components/MarkdownEditor.css | 622 ++++++++++++++++++ .../src/components/MarkdownEditor.js | 147 +++++ package.json | 1 + yarn.lock | 290 ++------ 4 files changed, 827 insertions(+), 233 deletions(-) create mode 100644 client/coral-admin/src/components/MarkdownEditor.css create mode 100644 client/coral-admin/src/components/MarkdownEditor.js diff --git a/client/coral-admin/src/components/MarkdownEditor.css b/client/coral-admin/src/components/MarkdownEditor.css new file mode 100644 index 000000000..93bf7a9dd --- /dev/null +++ b/client/coral-admin/src/components/MarkdownEditor.css @@ -0,0 +1,622 @@ +$minHeight: 200px; +$fullscreenZIndex: 10; + +.wrapper { + /* Fix blockquote styling of MDL: https://github.com/google/material-design-lite/issues/2037 */ + blockquote { + > p:first-child { + padding-top: 16px; + } + > p:last-child { + margin-bottom: 0; + } + &::after { + margin-left: -0.5em; + } + } +} + +.iconBold { + composes: material-icons from global; + &::before { + content: 'format_bold'; + } +} + +.iconItalic { + composes: material-icons from global; + &::before { + content: 'format_italic'; + } +} + +.iconTitle { + composes: material-icons from global; + &::before { + content: 'title'; + } +} + +.iconQuote { + composes: material-icons from global; + &::before { + content: 'format_quote'; + } +} + +.iconUnorderedList { + composes: material-icons from global; + &::before { + content: 'format_list_bulleted'; + } +} + +.iconOrderedList { + composes: material-icons from global; + &::before { + content: 'format_list_numbered'; + } +} + +.iconLink { + composes: material-icons from global; + &::before { + content: 'link'; + } +} + +.iconImage { + composes: material-icons from global; + &::before { + content: 'insert_photo'; + } +} + +.iconPreview { + composes: material-icons from global; + &::before { + content: 'remove_red_eye'; + } +} + +.iconSideBySide { + composes: material-icons from global; + &::before { + content: 'chrome_reader_mode'; + } +} + +.iconFullscreen { + composes: material-icons from global; + &::before { + content: 'fullscreen'; + } +} + +.iconGuide { + composes: material-icons from global; + &::before { + content: 'help_outline'; + } +} + +/* + * These are modified styles taken from https://github.com/NextStepWebs/simplemde-markdown-editor and + * put through http://sebastianpontow.de/css2compass/. + */ + +/* + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +:global { + .CodeMirror { + font-family: monospace; + color: black; + position: relative; + overflow: hidden; + background: white; + height: auto; + min-height: $minHeight; + border: 1px solid #ddd; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding: 10px; + font: inherit; + z-index: 1; + pre { + padding: 0 4px; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + font-variant-ligatures: none; + } + span { + /*TODO: vertical-align: text-bottom;*/ + } + .CodeMirror-code { + .cm-tag { + color: #63a35c; + } + .cm-attribute { + color: #795da3; + } + .cm-string { + color: #183691; + } + .cm-header-1 { + font-size: 200%; + line-height: 200%; + } + .cm-header-2 { + font-size: 160%; + line-height: 160%; + } + .cm-header-3 { + font-size: 125%; + line-height: 125%; + } + .cm-header-4 { + font-size: 110%; + line-height: 110%; + } + .cm-comment { + background: rgba(0, 0, 0, .05); + border-radius: 2px; + } + .cm-link { + color: #7f8c8d; + } + .cm-url { + color: #aab2b3; + } + .cm-strikethrough { + text-decoration: line-through; + } + .cm-tab { + display: inline-block; + text-decoration: inherit; + } + .CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; + } + .cm-header { + font-weight: bold; + } + .cm-strong { + font-weight: bold; + } + .cm-em { + font-style: italic; + } + .cm-link { + text-decoration: underline; + } + .cm-strikethrough { + text-decoration: line-through; + } + .cm-invalidchar { + color: #f00; + } + } + .CodeMirror-selected { + background: #d9d9d9; + } + .CodeMirror-placeholder { + opacity: .5; + } + div.CodeMirror-secondarycursor { + border-left: 1px solid silver; + } + .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) { + background: rgba(255, 0, 0, .15); + } + } + .CodeMirror-lines { + padding: 4px 0; + cursor: text; + min-height: 1px; + } + .CodeMirror-scrollbar-filler { + background-color: white; + position: absolute; + z-index: 6; + display: none; + right: 0; + bottom: 0; + } + .CodeMirror-gutter-filler { + background-color: white; + position: absolute; + z-index: 6; + display: none; + left: 0; + bottom: 0; + } + .CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; + position: absolute; + left: 0; + top: 0; + min-height: 100%; + z-index: 3; + box-sizing: content-box; + } + .CodeMirror-guttermarker { + color: black; + } + .CodeMirror-guttermarker-subtle { + color: #999; + } + .CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; + } + .CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; + position: absolute; + } + @keyframes blink { + 0% { + } + 50% { + background-color: transparent; + } + 100% { + } + } + .CodeMirror-scroll { + overflow: scroll !important; + margin-bottom: -30px; + margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; + position: relative; + min-height: $minHeight; + box-sizing: content-box; + } + .CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; + box-sizing: content-box; + } + .CodeMirror-vscrollbar { + position: absolute; + z-index: 6; + display: none; + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; + } + .CodeMirror-hscrollbar { + position: absolute; + z-index: 6; + display: none; + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; + } + .CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; + *zoom: 1; + *display: inline; + box-sizing: content-box; + } + .CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; + user-select: none; + } + .CodeMirror-gutter-background { + position: absolute; + top: 0; + bottom: 0; + z-index: 4; + } + .CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; + } + .CodeMirror-code { + outline: none; + } + .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; + pre { + position: static; + } + } + .CodeMirror-focused { + .CodeMirror-selected { + background: #d7d4f0; + } + div.CodeMirror-cursors { + visibility: visible; + } + } + .CodeMirror-selected { + background: #d9d9d9; + } + .CodeMirror-line::selection { + background: #d7d4f0; + } + .CodeMirror-line { + > span::selection { + background: #d7d4f0; + } + > span { + > span::selection { + background: #d7d4f0; + } + } + } + @media print { + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } + } + .CodeMirror-fullscreen { + background: #fff; + position: fixed !important; + top: 50px; + left: 0; + right: 0; + bottom: 0; + height: auto; + z-index: $fullscreenZIndex; + } + .CodeMirror-sided { + width: 50% !important; + } + .editor-toolbar { + position: relative; + opacity: .6; + user-select: none; + padding: 0 10px; + border-top: 1px solid #bbb; + border-left: 1px solid #bbb; + border-right: 1px solid #bbb; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + &:after { + display: block; + content: ' '; + height: 1px; + margin-top: 8px; + } + &:before { + display: block; + content: ' '; + height: 1px; + margin-bottom: 8px; + } + &:hover { + opacity: .8; + } + &.fullscreen { + width: 100%; + height: 50px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + background: #fff; + border: 0; + position: fixed; + top: 0; + left: 0; + opacity: 1; + z-index: $fullscreenZIndex; + } + &.fullscreen::before { + width: 20px; + height: 50px; + background: linear-gradient(to right, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%); + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + } + &.fullscreen::after { + width: 20px; + height: 50px; + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%); + position: fixed; + top: 0; + right: 0; + margin: 0; + padding: 0; + } + a { + display: inline-block; + text-align: center; + text-decoration: none!important; + color: #2c3e50!important; + width: 30px; + height: 30px; + margin: 0; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + outline: 0; + margin-right: 2px; + &.active { + background: #fcfcfc; + border-color: #95a5a6; + } + &:hover { + background: #fcfcfc; + border-color: #95a5a6; + } + &:active { + background: #eee; + } + &:before { + line-height: 30px; + } + } + i.separator { + display: inline-block; + width: 0; + border-left: 1px solid #d9d9d9; + border-right: 1px solid #fff; + color: transparent; + text-indent: -10px; + margin: 0 6px; + } + &.disabled-for-preview a:not(.no-disable) { + pointer-events: none; + background: #fff; + border-color: transparent; + text-shadow: inherit; + } + } + @media only screen and(max-width: 700px) { + .editor-toolbar a.no-mobile { + display: none; + } + } + .editor-statusbar { + padding: 8px 10px; + font-size: 12px; + color: #959694; + text-align: right; + span { + display: inline-block; + min-width: 4em; + margin-left: 1em; + } + .lines:before { + content: 'lines: '; + } + .words:before { + content: 'words: '; + } + .characters:before { + content: 'characters: '; + } + } + .editor-preview { + padding: 10px; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: #fafafa; + z-index: 7; + overflow: auto; + display: none; + box-sizing: border-box; + > p { + margin-top: 0; + } + pre { + background: #eee; + margin-bottom: 10px; + } + table { + td { + border: 1px solid #ddd; + padding: 5px; + } + th { + border: 1px solid #ddd; + padding: 5px; + } + } + } + .editor-preview-side { + padding: 10px; + position: fixed; + bottom: 0; + width: 50%; + top: 50px; + right: 0; + background: #fafafa; + z-index: $fullscreenZIndex; + overflow: auto; + display: none; + box-sizing: border-box; + border: 1px solid #ddd; + > p { + margin-top: 0; + } + pre { + background: #eee; + margin-bottom: 10px; + } + table { + td { + border: 1px solid #ddd; + padding: 5px; + } + th { + border: 1px solid #ddd; + padding: 5px; + } + } + } + .editor-preview-active-side { + display: block; + } + .editor-preview-active { + display: block; + } + .CodeMirror-overwrite .CodeMirror-cursor { + } + .CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; + } + .cm-tab-wrap-hack:after { + content: ''; + } + span.CodeMirror-selectedtext { + background: none; + } + .editor-wrapper input.title { + &:focus { + opacity: .8; + } + &:hover { + opacity: .8; + } + } +} diff --git a/client/coral-admin/src/components/MarkdownEditor.js b/client/coral-admin/src/components/MarkdownEditor.js new file mode 100644 index 000000000..0d2088e51 --- /dev/null +++ b/client/coral-admin/src/components/MarkdownEditor.js @@ -0,0 +1,147 @@ +import React, {Component, PropTypes} from 'react'; +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, + }); + 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 ( +
+