From d99f6d89234cc1f350ce14986c55a4ae64305cd8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:44:22 +0700 Subject: [PATCH 1/3] Support lazily resolving slot fragments --- .../routes/Moderation/containers/Comment.js | 12 ++- .../Moderation/containers/UserDetail.js | 14 ++-- .../src/containers/Comment.js | 12 ++- .../src/containers/Stream.js | 13 ++- client/coral-framework/helpers/plugins.js | 64 +++----------- client/coral-framework/hocs/withQuery.js | 36 +++++++- .../services/graphqlRegistry.js | 84 +++++++++++++++---- client/coral-framework/utils/index.js | 5 ++ 8 files changed, 138 insertions(+), 102 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/containers/Comment.js b/client/coral-admin/src/routes/Moderation/containers/Comment.js index 8843188b8..d724682ab 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Comment.js +++ b/client/coral-admin/src/routes/Moderation/containers/Comment.js @@ -1,22 +1,21 @@ import {gql} from 'react-apollo'; import Comment from '../components/Comment'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; import withFragments from 'coral-framework/hocs/withFragments'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; -const pluginFragments = getSlotsFragments([ +const slots = [ 'adminCommentInfoBar', 'adminCommentContent', 'adminSideActions', 'adminCommentDetailArea', -]); +]; export default withFragments({ root: gql` fragment CoralAdmin_ModerationComment_root on RootQuery { __typename - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${pluginFragments.definitions('root')} `, comment: gql` fragment CoralAdmin_ModerationComment_comment on Comment { @@ -52,8 +51,7 @@ export default withFragments({ editing { edited } - ${pluginFragments.spreads('comment')} + ${getSlotFragmentSpreads(slots, 'comment')} } - ${pluginFragments.definitions('comment')} ` })(Comment); diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js index 0e36ed51a..6b476b861 100644 --- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js @@ -4,8 +4,7 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import UserDetail from '../components/UserDetail'; import withQuery from 'coral-framework/hocs/withQuery'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; -import {getDefinitionName} from 'coral-framework/utils'; +import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import { changeUserDetailStatuses, clearUserDetailSelections, @@ -26,9 +25,9 @@ const commentConnectionFragment = gql` ${Comment.fragments.comment} `; -const pluginFragments = getSlotsFragments([ +const slots = [ 'userProfile', -]); +]; class UserDetailContainer extends React.Component { static propTypes = { @@ -80,7 +79,7 @@ export const withUserDetailQuery = withQuery(gql` id provider } - ${pluginFragments.spreads('user')} + ${getSlotFragmentSpreads(slots, 'user')} } totalComments: commentCount(query: {author_id: $author_id}) rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]}) @@ -90,11 +89,8 @@ export const withUserDetailQuery = withQuery(gql` }) { ...CoralAdmin_Moderation_CommentConnection } - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${Comment.fragments.comment} - ${pluginFragments.definitions('user')} - ${pluginFragments.definitions('root')} ${commentConnectionFragment} `, { options: ({id, moderation: {userDetailStatuses: statuses}}) => { diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 1ffac82cf..479c6f302 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -1,9 +1,9 @@ import {gql} from 'react-apollo'; import Comment from '../components/Comment'; import {withFragments} from 'coral-framework/hocs'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; -const pluginFragments = getSlotsFragments([ +const slots = [ 'streamQuestionArea', 'commentInputArea', 'commentInputDetailArea', @@ -12,15 +12,14 @@ const pluginFragments = getSlotsFragments([ 'commentContent', 'commentReactions', 'commentAvatar' -]); +]; export default withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { __typename - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} } - ${pluginFragments.definitions('root')} `, comment: gql` fragment CoralEmbedStream_Comment_comment on Comment { @@ -48,8 +47,7 @@ export default withFragments({ edited editableUntil } - ${pluginFragments.spreads('comment')} + ${getSlotFragmentSpreads(slots, 'comment')} } - ${pluginFragments.definitions('comment')} ` })(Comment); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 994d749f3..a6fb4656f 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -15,9 +15,8 @@ import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/strea import Stream from '../components/Stream'; import Comment from './Comment'; import {withFragments} from 'coral-framework/hocs'; -import {getSlotsFragments} from 'coral-framework/helpers/plugins'; +import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {Spinner} from 'coral-ui'; -import {getDefinitionName} from 'coral-framework/utils'; import { findCommentInEmbedQuery, insertCommentIntoEmbedQuery, @@ -231,10 +230,10 @@ const LOAD_MORE_QUERY = gql` ${Comment.fragments.comment} `; -const pluginFragments = getSlotsFragments([ +const slots = [ 'streamTabs', 'streamTabPanes', -]); +]; const fragments = { root: gql` @@ -277,7 +276,7 @@ const fragments = { startCursor endCursor } - ${pluginFragments.spreads('asset')} + ${getSlotFragmentSpreads(slots, 'asset')} } me { status @@ -288,11 +287,9 @@ const fragments = { settings { organizationName } - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} ...${getDefinitionName(Comment.fragments.root)} } - ${pluginFragments.definitions('asset')} - ${pluginFragments.definitions('root')} ${Comment.fragments.root} ${commentFragment} `, diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 625a0ee3f..d60329baa 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -5,7 +5,6 @@ import merge from 'lodash/merge'; import plugins from 'pluginsConfig'; import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; -import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import {loadTranslations} from 'coral-framework/services/i18n'; import {injectReducers, getStore} from 'coral-framework/services/store'; import camelize from './camelize'; @@ -35,62 +34,21 @@ export function getSlotElements(slot, props = {}) { .map((component, i) => React.createElement(component, {key: i, ...props})); } -function getComponentFragments(components) { - const res = components +export function getSlotFragments(slot, part) { + const components = uniq(flattenDeep(plugins + .filter((o) => o.module.slots ? o.module.slots[slot] : false) + .map((o) => o.module.slots[slot]) + )); + + const documents = components .map((c) => c.fragments) - .filter((fragments) => fragments) + .filter((fragments) => fragments && fragments[part]) .reduce((res, fragments) => { - Object.keys(fragments).forEach((key) => { - if (!(key in res)) { - res[key] = {spreads: [], definitions: []}; - } - res[key].spreads.push(getDefinitionName(fragments[key])); - res[key].definitions.push(fragments[key]); - }); + res.push(fragments[part]); return res; - }, {}); + }, []); - Object.keys(res).forEach((key) => { - - // Assemble arguments for `gql` to call it directly without using template literals. - res[key].spreads = `...${res[key].spreads.join('\n...')}\n`; - res[key].definitions = mergeDocuments(res[key].definitions); - }); - - return res; -} - -/** - * Returns an object that can be used to compose fragments or queries. - * - * Example: - * const pluginFragments = getSlotsFragments(['commentInfoBar', 'commentActions']); - * const rootFragment = gql` - * fragment Comment_root on RootQuery { - + ${pluginFragments.spreads('root')} - * } - * ${pluginFragments.definitions('root')} - * `; - */ -export function getSlotsFragments(slots) { - if (!Array.isArray(slots)) { - slots = [slots]; - } - const components = uniq(flattenDeep(slots.map((slot) => { - return plugins - .filter((o) => o.module.slots ? o.module.slots[slot] : false) - .map((o) => o.module.slots[slot]); - }))); - - const fragments = getComponentFragments(components); - return { - spreads(key) { - return (fragments[key] && fragments[key].spreads) || ''; - }, - definitions(key) { - return (fragments[key] && fragments[key].definitions) || ''; - }, - }; + return documents; } export function getGraphQLExtensions() { diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index c4e9bc375..ec190466c 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -15,14 +15,42 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => { * apply query options registered in the graphRegistry. */ export default (document, config = {}) => (WrappedComponent) => { - config = { + const wrappedConfig = { ...config, options: config.options || {}, - props: config.props || (({data}) => separateDataAndRoot(data)), + props: (args) => { + const wrappedArgs = { + ...args, + data: { + ...args.data, + subscribeToMore(stmArgs) { + + // Resolve document fragments before passing it to `apollo-client`. + return args.data.subscribeToMore({ + ...stmArgs, + document: resolveFragments(stmArgs.document), + }); + }, + fetchMore(lmArgs) { + + // Resolve document fragments before passing it to `apollo-client`. + return args.data.fetchMore({ + ...lmArgs, + query: resolveFragments(lmArgs.query), + }); + }, + }, + }; + return config.props + ? config.props(wrappedArgs) + : separateDataAndRoot(wrappedArgs.data); + }, }; const wrappedOptions = (data) => { - const base = (typeof config.options === 'function') ? config.options(data) : config.options; + const base = (typeof wrappedConfig.options === 'function') + ? wrappedConfig.options(data) + : wrappedConfig.options; const name = getDefinitionName(document); const configs = getQueryOptions(name); const reducerCallbacks = @@ -45,7 +73,7 @@ export default (document, config = {}) => (WrappedComponent) => { let memoized = null; const getWrapped = () => { if (!memoized) { - memoized = graphql(resolveFragments(document), {...config, options: wrappedOptions})(WrappedComponent); + memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent); } return memoized; }; diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 8b917c465..41d3e2d10 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -1,7 +1,8 @@ import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; -import {getGraphQLExtensions} from 'coral-framework/helpers/plugins'; +import {getGraphQLExtensions, getSlotFragments} from 'coral-framework/helpers/plugins'; import globalFragments from 'coral-framework/graphql/fragments'; import uniq from 'lodash/uniq'; +import {gql} from 'react-apollo'; const fragments = {}; const mutationOptions = {}; @@ -139,18 +140,47 @@ export function getQueryOptions(key) { } /** - * Get a document with a fragment named `key`, which contains - * all fragments added under this key. + * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. + * It parses the slot name and the resource and usees the plugin API to assemble + * the fragment document. */ -export function getFragmentDocument(key) { - init(); +function getSlotFragmentDocument(key) { + const match = key.match(/TalkSlot_(.*)_(.*)/); + if (!match) { + return ''; + } + const slot = match[1][0].toLowerCase() + match[1].substr(1); + const resource = match[2]; + const documents = getSlotFragments(slot, resource); + + if (documents.length === 0) { + return ''; + } + + const names = documents.map((d) => getDefinitionName(d)); + const typeName = getTypeName(documents[0]); + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${typeName} { + ...${names.join('\n...')}\n + } + `; + return mergeDocuments([main, ...documents]); +} + +/** + * getRegistryFragmentDocument assembles a fragment document using + * all registered fragment under given `key`. + */ +function getRegistryFragmentDocument(key) { if (!(key in fragments)) { return ''; } - let documents = fragments[key] ? fragments[key].documents : []; - let fields = fragments[key] ? `...${fragments[key].names.join('\n...')}\n` : ' __typename'; + let documents = fragments[key].documents; + let fields = `...${fragments[key].names.join('\n...')}\n`; // Assemble arguments for `gql` to call it directly without using template literals. const main = ` @@ -161,6 +191,16 @@ export function getFragmentDocument(key) { return mergeDocuments([main, ...documents]); } +/** + * getFragmentDocument returns a fragment that assembles all registered + * fragments under given `key` or if `key` refers to Slot fragments it will + * return the slot fragments specified by this key. + */ +export function getFragmentDocument(key) { + init(); + return getRegistryFragmentDocument(key) || getSlotFragmentDocument(key); +} + // The fragments and configs are lazily loaded to allow circular dependencies to work. // TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations. let initialized = false; @@ -178,19 +218,35 @@ function init() { getGraphQLExtensions().forEach((ext) => add(ext)); } +/** + * resolveFragments finds fragment spread names and attachs + * the related fragment document to the given root document. + */ export function resolveFragments(document) { if (document.loc.source) { - // resolve fragments from registry - const matchedSubFragments = document.loc.source.body.match(/\.\.\.(.*)/g) || []; - const subFragments = - uniq(matchedSubFragments.map((f) => f.replace('...', ''))) - .map((key) => getFragmentDocument(key)) - .filter((i) => i); + const subFragments = []; + let body = document.loc.source.body; + + const matchedSubFragments = body.match(/\.\.\.(.*)/g) || []; + + uniq(matchedSubFragments.map((f) => f.replace('...', ''))) + .forEach((key) => { + const doc = getFragmentDocument(key); + if (doc) { + subFragments.push(doc); + } else if(key.startsWith('TalkSlot_')) { + + // Remove fragment spread of slots with zero fragments. + body = body.replace(`...${key}\n`, ''); + } + }); if (subFragments.length > 0) { - return mergeDocuments([document, ...subFragments]); + return mergeDocuments([body, ...subFragments]); } + + return gql`${body}`; } else { console.warn('Can only resolve fragments from documents definied using the gql tag.'); } diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 1866005be..0608179ed 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -1,5 +1,6 @@ import {gql} from 'react-apollo'; import t from 'coral-framework/services/i18n'; +import {capitalize} from 'coral-framework/helpers/strings'; export const getTotalActionCount = (type, comment) => { return comment.action_summaries @@ -144,3 +145,7 @@ export function forEachError(error, callback) { callback({error: e, msg}); }); } + +export function getSlotFragmentSpreads(slots, part) { + return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${part}`).join('\n...')}\n`; +} From 5fee26da0e35714df89399aef55ab226963e44de Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:50:00 +0700 Subject: [PATCH 2/3] Document getSlotFragmentSpreads --- client/coral-framework/utils/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 0608179ed..73a6a6676 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -146,6 +146,12 @@ export function forEachError(error, callback) { }); } -export function getSlotFragmentSpreads(slots, part) { - return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${part}`).join('\n...')}\n`; +/** + * getSlotFragmentSpreads will return a string in the + * expected format for slot fragments, given `slots` and `resource`. + * e.g. `getSlotsFragmentSpreads(['slotName'], 'root')` returns + * `...TalkSlot_SlotName_root`. + */ +export function getSlotFragmentSpreads(slots, resource) { + return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${resource}`).join('\n...')}\n`; } From 52cd7bcce4ca2af214e576f2102c06c6f572661c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 19 Jul 2017 20:51:21 +0700 Subject: [PATCH 3/3] Expose getSlotFragmentSpread in plugin-api --- client/coral-framework/utils/index.js | 2 +- plugin-api/beta/client/utils/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 plugin-api/beta/client/utils/index.js diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 73a6a6676..61413e03d 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -149,7 +149,7 @@ export function forEachError(error, callback) { /** * getSlotFragmentSpreads will return a string in the * expected format for slot fragments, given `slots` and `resource`. - * e.g. `getSlotsFragmentSpreads(['slotName'], 'root')` returns + * e.g. `getSlotFragmentSpreads(['slotName'], 'root')` returns * `...TalkSlot_SlotName_root`. */ export function getSlotFragmentSpreads(slots, resource) { diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js new file mode 100644 index 000000000..44c3af6c0 --- /dev/null +++ b/plugin-api/beta/client/utils/index.js @@ -0,0 +1 @@ +export {getSlotFragmentSpreads} from 'coral-framework/utils';