Merge branch 'resolve-slot-fragments-lazy' into featured-comments

Conflicts:
	client/coral-embed-stream/src/containers/Stream.js
	client/coral-framework/utils/index.js
	plugin-api/beta/client/utils/index.js
This commit is contained in:
Chi Vinh Le
2017-07-19 21:15:35 +07:00
9 changed files with 146 additions and 104 deletions
@@ -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);
@@ -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}}) => {
@@ -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);
@@ -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}
+11 -53
View File
@@ -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() {
+32 -4
View File
@@ -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;
};
@@ -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.');
}
+11
View File
@@ -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`;
}
+1
View File
@@ -1 +1,2 @@
export {isTagged, insertCommentsSorted} from 'coral-framework/utils';
export {getSlotFragmentSpreads} from 'coral-framework/utils';