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 8f660c3ca..5d43e9eb7 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,22 +12,20 @@ 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')} `, asset: gql` fragment CoralEmbedStream_Comment_asset on Asset { __typename - ${pluginFragments.spreads('asset')} + ${getSlotFragmentSpreads(slots, 'asset')} } - ${pluginFragments.definitions('asset')} `, comment: gql` fragment CoralEmbedStream_Comment_comment on Comment { @@ -56,8 +54,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 1504227dc..6738cbd79 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, @@ -229,10 +228,10 @@ const LOAD_MORE_QUERY = gql` ${Comment.fragments.comment} `; -const pluginFragments = getSlotsFragments([ +const slots = [ 'streamTabs', 'streamTabPanes', -]); +]; const fragments = { root: gql` @@ -275,7 +274,7 @@ const fragments = { startCursor endCursor } - ${pluginFragments.spreads('asset')} + ${getSlotFragmentSpreads(slots, 'asset')} ...${getDefinitionName(Comment.fragments.asset)} } me { @@ -287,11 +286,9 @@ const fragments = { settings { organizationName } - ${pluginFragments.spreads('root')} + ${getSlotFragmentSpreads(slots, 'root')} ...${getDefinitionName(Comment.fragments.root)} } - ${pluginFragments.definitions('asset')} - ${pluginFragments.definitions('root')} ${Comment.fragments.asset} ${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 974be9502..3768b3940 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 @@ -176,3 +177,13 @@ export function buildUrl({protocol, hostname, port, pathname, search, hash} = wi } return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}${search}${hash}`; } + +/** + * getSlotFragmentSpreads will return a string in the + * expected format for slot fragments, given `slots` and `resource`. + * e.g. `getSlotFragmentSpreads(['slotName'], 'root')` returns + * `...TalkSlot_SlotName_root`. + */ +export function getSlotFragmentSpreads(slots, resource) { + return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${resource}`).join('\n...')}\n`; +} diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index edbd06fa6..aa63ccad2 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -1 +1,2 @@ export {isTagged, insertCommentsSorted} from 'coral-framework/utils'; +export {getSlotFragmentSpreads} from 'coral-framework/utils';