diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 7ce143c55..87412c0a6 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -54,7 +54,7 @@ class Comment extends React.Component { parentId: PropTypes.string, highlighted: PropTypes.string, addNotification: PropTypes.func.isRequired, - postItem: PropTypes.func.isRequired, + postComment: PropTypes.func.isRequired, depth: PropTypes.number.isRequired, asset: PropTypes.shape({ id: PropTypes.string, @@ -107,7 +107,7 @@ class Comment extends React.Component { currentUser, asset, depth, - postItem, + postComment, addNotification, showSignInDialog, postLike, @@ -268,7 +268,7 @@ class Comment extends React.Component { parentId={parentId || comment.id} addNotification={addNotification} authorId={currentUser.id} - postItem={postItem} + postComment={postComment} assetId={asset.id} /> : null } @@ -285,7 +285,7 @@ class Comment extends React.Component { activeReplyBox={activeReplyBox} addNotification={addNotification} parentId={comment.id} - postItem={postItem} + postComment={postComment} depth={depth + 1} asset={asset} highlighted={highlighted} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index eb30dc16c..96e571a01 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -26,7 +26,7 @@ class Stream extends React.Component { render() { const { root: {asset, asset: {comments}, comment, myIgnoredUsers}, - postItem, + postComment, addNotification, postFlag, postLike, @@ -85,7 +85,7 @@ class Stream extends React.Component { {user ? ({ variables: { assetId, @@ -85,7 +86,6 @@ export const withQuery = graphql(EMBED_QUERY, { excludeIgnored: Boolean(auth && auth.user && auth.user.id), }, }), - props: ({data}) => separateDataAndRoot(data), }); const mapStateToProps = state => ({ @@ -113,6 +113,6 @@ export default compose( props => !props.auth.checkedInitialLogin, renderComponent(Spinner), ), - withQuery, + withEmbedQuery, )(EmbedContainer); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 49f234dab..2253a6a82 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -6,13 +6,13 @@ import uniqBy from 'lodash/uniqBy'; import sortBy from 'lodash/sortBy'; import isNil from 'lodash/isNil'; import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream'; -import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; +import {withPostComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; import {notificationActions, authActions} from 'coral-framework'; import {editName} from 'coral-framework/actions/user'; import {setCommentCountCache, setActiveReplyBox} from '../actions/stream'; import Stream from '../components/Stream'; import Comment from './Comment'; -import withFragments from 'coral-framework/hocs/withFragments'; +import {withFragments} from 'coral-framework/hocs'; import {getDefinitionName} from 'coral-framework/utils'; const {showSignInDialog} = authActions; @@ -237,7 +237,7 @@ const mapDispatchToProps = dispatch => export default compose( withFragments(fragments), connect(mapStateToProps, mapDispatchToProps), - postComment, + withPostComment, postFlag, postLike, postDontAgree, diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js new file mode 100644 index 000000000..2d6f943a2 --- /dev/null +++ b/client/coral-embed-stream/src/graphql/index.js @@ -0,0 +1,105 @@ +import {gql} from 'react-apollo'; +import {registerConfig} from 'coral-framework/services/registry'; + +const config = { + fragments: { + CreateCommentResponse: gql` + fragment Coral_CreateCommentResponse on CreateCommentResponse { + comment { + ...Coral_CreateCommentResponse_Comment + replies { + ...Coral_CreateCommentResponse_Comment + } + } + errors { + translation_key + } + } + + fragment Coral_CreateCommentResponse_Comment on Comment { + id + body + created_at + status + replyCount + tags { + name + } + user { + id + name: username + } + action_summaries { + count + current_user { + id + created_at + } + } + }`, + }, + mutations: { + PostComment: ({ + variables: {comment: {asset_id, body, parent_id, tags = []}}, + state: {auth}, + }) => ({ + optimisticResponse: { + createComment: { + comment: { + user: { + id: auth.toJS().user.id, + name: auth.toJS().user.username + }, + created_at: new Date().toISOString(), + body, + parent_id, + asset_id, + action_summaries: [], + tags, + status: null, + id: 'pending' + } + } + }, + updateQueries: { + EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => { + if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { + return oldData; + } + + let updatedAsset; + + // If posting a reply + if (parent_id) { + updatedAsset = { + ...oldData, + asset: { + ...oldData.asset, + comments: oldData.asset.comments.map((oldComment) => { + return oldComment.id === parent_id + ? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1} + : oldComment; + }) + } + }; + } else { + + // If posting a top-level comment + updatedAsset = { + ...oldData, + asset: { + ...oldData.asset, + commentCount: oldData.asset.commentCount + 1, + comments: [comment, ...oldData.asset.comments] + } + }; + } + + return updatedAsset; + } + } + }) + }, +}; + +registerConfig(config); diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 2fd1e2731..bd63ba267 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -4,6 +4,7 @@ import {ApolloProvider} from 'react-apollo'; import {client} from 'coral-framework/services/client'; import {checkLogin} from 'coral-framework/actions/auth'; +import './graphql'; import reducers from './reducers'; import localStore, {injectReducers} from 'coral-framework/services/store'; diff --git a/client/coral-framework/graphql/fragments/index.js b/client/coral-framework/graphql/fragments/index.js new file mode 100644 index 000000000..f4948ed05 --- /dev/null +++ b/client/coral-framework/graphql/fragments/index.js @@ -0,0 +1,2 @@ +// fragments defined here are automatically registered. +export default {}; diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index b1ea57ae1..56f63b372 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -1,5 +1,4 @@ -import {graphql} from 'react-apollo'; -import POST_COMMENT from './postComment.graphql'; +import {graphql, gql} from 'react-apollo'; import POST_FLAG from './postFlag.graphql'; import POST_LIKE from './postLike.graphql'; import POST_DONT_AGREE from './postDontAgree.graphql'; @@ -8,80 +7,28 @@ import ADD_COMMENT_TAG from './addCommentTag.graphql'; import REMOVE_COMMENT_TAG from './removeCommentTag.graphql'; import IGNORE_USER from './ignoreUser.graphql'; import STOP_IGNORING_USER from './stopIgnoringUser.graphql'; +import withMutation from '../../hocs/withMutation'; +import {getFragmentDocument} from '../../services/registry'; -import commentView from '../fragments/commentView.graphql'; - -export const postComment = graphql(POST_COMMENT, { - options: () => ({ - fragments: commentView - }), - props: ({ownProps, mutate}) => ({ - postItem: comment => { - const {asset_id, body, parent_id, tags = []} = comment; - return mutate({ - variables: { - comment - }, - optimisticResponse: { - createComment: { - comment: { - user: { - id: ownProps.auth.user.id, - name: ownProps.auth.user.username - }, - created_at: new Date().toISOString(), - body, - parent_id, - asset_id, - action_summaries: [], - tags, - status: null, - id: 'pending' - } - } - }, - updateQueries: { - EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => { - - if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { - return oldData; - } - - let updatedAsset; - - // If posting a reply - if (parent_id) { - updatedAsset = { - ...oldData, - asset: { - ...oldData.asset, - comments: oldData.asset.comments.map((oldComment) => { - return oldComment.id === parent_id - ? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1} - : oldComment; - }) - } - }; - } else { - - // If posting a top-level comment - updatedAsset = { - ...oldData, - asset: { - ...oldData.asset, - commentCount: oldData.asset.commentCount + 1, - comments: [comment, ...oldData.asset.comments] - } - }; - } - - return updatedAsset; - } - } - }); +export const withPostComment = withMutation( + gql` + mutation PostComment($comment: CreateCommentInput!) { + createComment(comment: $comment) { + ...CreateCommentResponse + } } - }), -}); + ${getFragmentDocument('CreateCommentResponse')} + `, { + props: ({mutate}) => ({ + postComment: comment => { + return mutate({ + variables: { + comment + }, + }); + } + }), + }); export const postLike = graphql(POST_LIKE, { props: ({mutate}) => ({ diff --git a/client/coral-framework/graphql/mutations/postComment.graphql b/client/coral-framework/graphql/mutations/postComment.graphql deleted file mode 100644 index f98558804..000000000 --- a/client/coral-framework/graphql/mutations/postComment.graphql +++ /dev/null @@ -1,16 +0,0 @@ -#import "../fragments/commentView.graphql" - -mutation CreateComment ($comment: CreateCommentInput!) { - createComment(comment: $comment) { - comment { - ...commentView - replyCount - replies { - ...commentView - } - } - errors { - translation_key - } - } -} diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 20aee7abb..76995c204 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -3,6 +3,7 @@ import merge from 'lodash/merge'; import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; import uniq from 'lodash/uniq'; +import pick from 'lodash/pick'; import plugins from 'pluginsConfig'; import {gql} from 'react-apollo'; import {getDefinitionName} from 'coral-framework/utils'; @@ -25,19 +26,27 @@ export function getSlotElements(slot, props = {}) { } function getComponentFragments(components) { - return components + const res = components .map(c => c.fragments) .filter(fragments => fragments) .reduce((res, fragments) => { Object.keys(fragments).forEach(key => { if (!(key in res)) { - res[key] = {spreads: '', definitions: ''}; + res[key] = {spreads: [], definitions: []}; } - res[key].spreads += `...${getDefinitionName(fragments[key])}\n`; - res[key].definitions = gql`${res[key].definitions}${fragments[key]}`; + res[key].spreads.push(getDefinitionName(fragments[key])); + res[key].definitions.push(fragments[key]); }); return res; }, {}); + + Object.keys(res).forEach(key => { + res[key].spreads = `...${res[key].spreads.join('\n...')}\n`; + const literals = ['', ...res[key].definitions.map(() => '\n')]; + res[key].definitions = gql.apply(null, [literals, ...res[key].definitions]); + }); + + return res; } /** @@ -73,3 +82,9 @@ export function getSlotsFragments(slots) { }; } +export function getGraphQLConfigs() { + return plugins + .map(o => o.module.mutations && pick(o.module, ['mutations', 'queries', 'fragments'])) + .filter(o => o); +} + diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js new file mode 100644 index 000000000..d624a3308 --- /dev/null +++ b/client/coral-framework/hocs/index.js @@ -0,0 +1,3 @@ +export {default as withFragments} from './withFragments'; +export {default as withMutation} from './withMutation'; +export {default as withQuery} from './withQuery'; diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js new file mode 100644 index 000000000..423ab9793 --- /dev/null +++ b/client/coral-framework/hocs/withMutation.js @@ -0,0 +1,74 @@ +import {graphql} from 'react-apollo'; +import merge from 'lodash/merge'; +import uniq from 'lodash/uniq'; +import {getMutationOptions} from 'coral-framework/services/registry'; +import {store} from 'coral-framework/services/store'; +import {getDefinitionName} from '../utils'; + +export default (definitions, config) => WrappedComponent => { + config = { + ...config, + options: config.options || {}, + props: config.props || (data => ({mutate: data.mutate()})), + }; + const wrappedProps = (data) => { + const name = getDefinitionName(definitions); + const callbacks = getMutationOptions(name); + const mutate = (base) => { + const variables = base.variables || config.options.variables; + const configs = callbacks.map(cb => cb({variables, state: store.getState()})); + + const optimisticResponse = merge( + base.optimisticResponse || config.options.optimisticResponse, + ...configs.map(cfg => cfg.optimisticResponse), + ); + + const refetchQueries = uniq( + base.refetchQueries || config.options.refetchQueries, + ...configs.map(cfg => cfg.refetchQueries), + ); + + const updateCallbacks = + [base.update || config.options.update] + .concat(...configs.map(cfg => cfg.update)) + .filter(i => i); + + const update = (proxy, result) => { + updateCallbacks.forEach(cb => cb(proxy, result)); + }; + + const updateQueries = + [ + base.updateQueries || config.options.updateQueries, + ...configs.map(cfg => cfg.updateQueries) + ] + .filter(i => i) + .reduce((res, map) => { + Object.keys(map).forEach(key => { + if (!(key in res)) { + res[key] = map[key]; + } else { + const existing = res[key]; + res[key] = (prev, result) => { + const next = existing(prev, result); + return map[key](next, result); + }; + } + }); + return res; + }, {}); + + const wrappedConfig = { + variables, + optimisticResponse, + refetchQueries, + updateQueries, + update, + }; + return data.mutate(wrappedConfig); + }; + return config.props({...data, mutate}); + }; + const wrapped = graphql(definitions, {...config, props: wrappedProps})(WrappedComponent); + return wrapped; +}; diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js new file mode 100644 index 000000000..9677c223e --- /dev/null +++ b/client/coral-framework/hocs/withQuery.js @@ -0,0 +1,34 @@ +import {graphql} from 'react-apollo'; +import {getQueryOptions} from 'coral-framework/services/registry'; +import {getDefinitionName, separateDataAndRoot} from '../utils'; + +export default (definitions, config) => WrappedComponent => { + config = { + ...config, + options: config.options || {}, + props: config.props || (({data}) => separateDataAndRoot(data)), + }; + + const wrappedOptions = (data) => { + const base = (typeof config.options === 'function') ? config.options(data) : config.options; + const name = getDefinitionName(definitions); + const configs = getQueryOptions(name); + const reducerCallbacks = + [base.reducer || (i => i)] + .concat(...configs.map(cfg => cfg.reducer)) + .filter(i => i); + + const reducer = reducerCallbacks.reduce( + (a, b) => (prev, ...rest) => + b(a(prev, ...rest), ...rest), + ); + + return { + ...base, + reducer, + }; + }; + + const wrapped = graphql(definitions, {...config, options: wrappedOptions})(WrappedComponent); + return wrapped; +}; diff --git a/client/coral-framework/services/registry.js b/client/coral-framework/services/registry.js new file mode 100644 index 000000000..2e889de94 --- /dev/null +++ b/client/coral-framework/services/registry.js @@ -0,0 +1,171 @@ +import {gql} from 'react-apollo'; +import {getDefinitionName} from 'coral-framework/utils'; +import {getGraphQLConfigs} from 'coral-framework/helpers/plugins'; +import globalFragments from 'coral-framework/graphql/fragments'; + +const fragments = {}; +const mutationOptions = {}; +const queryOptions = {}; + +const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value; + +/** + * Register fragment + * + * Example: + * registerFragment('MyFragment', gql` + * fragment Plugin_MyFragment on Comment { + * body + * } + * `); + */ +export function registerFragment(key, document) { + const type = getTypeName(document); + const name = getDefinitionName(document); + if (!(key in fragments)) { + fragments[key] = {type, names: [name], documents: [document]}; + } else { + if (type !== fragments[key].type) { + console.error(`Type mismatch ${type} !== ${fragments[key].type}`); + } + fragments[key].names.push(name); + fragments[key].documents.push(document); + } +} + +/** + * Register mutation options. + * + * Example: + * registerMutationOptions('PostComment', ({variables, state}) => ({ + * optimisticResponse: { + * CreateComment: { + * extra: '', + * }, + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + */ +export function registerMutationOptions(key, config) { + if (!(key in mutationOptions)) { + mutationOptions[key] = [config]; + } else { + mutationOptions[key].push(config); + } +} + +/** + * Register query options. + * + * Example: + * registerQueryOptions('EmbedQuery', { + * reducer: (previousResult, action, variables) => previousResult, + * }); + */ +export function registerQueryOptions(key, config) { + if (!(key in queryOptions)) { + queryOptions[key] = [config]; + } else { + queryOptions[key].push(config); + } +} + +/** + * Register all fragments, mutation options, and query options defined in the object. + * + * Example: + * registerConfig({ + * fragments: { + * CreateCommentResponse: gql` + * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { + * [...] + * }`, + * }, + * mutations: { + * PostComment: ({variables, state}) => ({ + * optimisticResponse: { + * [...] + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + * }, + * queries: { + * EmbedQuery: { + * reducer: (previousResult, action, variables) => { + * return previousResult; + * }, + * }, + * }, + * }); + */ +export function registerConfig(cfg) { + Object.keys(cfg.fragments || []).forEach(key => registerFragment(key, cfg.fragments[key])); + Object.keys(cfg.mutations || []).forEach(key => registerMutationOptions(key, cfg.mutations[key])); + Object.keys(cfg.queries || []).forEach(key => registerQueryOptions(key, cfg.queries[key])); +} + +/** + * Get a list of mutation options. + */ +export function getMutationOptions(key) { + init(); + return mutationOptions[key] || []; +} + +/** + * Get a list of query options. + */ +export function getQueryOptions(key) { + init(); + return queryOptions[key] || []; +} + +/** + * Get a document with a fragment named `key`, which contains + * all fragments registered under this key. + */ +export function getFragmentDocument(key) { + init(); + let documents = fragments[key] ? fragments[key].documents : []; + let fields = fragments[key] ? `...${fragments[key].names.join('\n...')}\n` : ' __typename'; + + const main = ` + fragment ${key} on ${fragments[key].type} { + ${fields} + } + `; + const literals = [main, ...documents.map(() => '\n')]; + return gql.apply(null, [literals, ...documents]); +} + +// The fragments and configs are lazily loaded to allow circular dependencies to work. +// TODO: We might want to change this to an explicit register after we have lazy Queries and Mutations. +let initialized = false; + +function init() { + if (initialized) { return; } + initialized = true; + + // Register fragments from framework. + [globalFragments].forEach(map => + Object.keys(map).forEach(key => registerFragment(key, map[key])) + ); + + // Register configs from plugins. + getGraphQLConfigs().forEach(cfg => registerConfig(cfg)); +} + diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 5d0906b6e..e19224179 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -25,7 +25,7 @@ class CommentBox extends React.Component { postComment = () => { const { commentPostedHandler, - postItem, + postComment, setCommentCountCache, commentCountCache, isReply, @@ -46,7 +46,7 @@ class CommentBox extends React.Component { // Execute preSubmit Hooks this.state.hooks.preSubmit.forEach(hook => hook()); - postItem(comment, 'comments') + postComment(comment, 'comments') .then(({data}) => { const postedComment = data.createComment.comment; @@ -192,7 +192,7 @@ CommentBox.propTypes = { charCountEnable: PropTypes.bool.isRequired, maxCharCount: PropTypes.number, commentPostedHandler: PropTypes.func, - postItem: PropTypes.func.isRequired, + postComment: PropTypes.func.isRequired, cancelButtonClicked: PropTypes.func, assetId: PropTypes.string.isRequired, parentId: PropTypes.string, diff --git a/client/coral-plugin-replies/ReplyBox.js b/client/coral-plugin-replies/ReplyBox.js index d6e2b5631..97667f932 100644 --- a/client/coral-plugin-replies/ReplyBox.js +++ b/client/coral-plugin-replies/ReplyBox.js @@ -12,7 +12,7 @@ class ReplyBox extends Component { render() { const { styles, - postItem, + postComment, assetId, authorId, addNotification, @@ -32,7 +32,7 @@ class ReplyBox extends Component { addNotification={addNotification} authorId={authorId} assetId={assetId} - postItem={postItem} + postComment={postComment} isReply={true} /> ; } @@ -46,7 +46,7 @@ ReplyBox.propTypes = { parentId: PropTypes.string, addNotification: PropTypes.func.isRequired, authorId: PropTypes.string.isRequired, - postItem: PropTypes.func.isRequired, + postComment: PropTypes.func.isRequired, assetId: PropTypes.string.isRequired }; diff --git a/plugins/coral-plugin-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js index 38be8a978..fcb7b2a57 100644 --- a/plugins/coral-plugin-respect/client/containers/RespectButton.js +++ b/plugins/coral-plugin-respect/client/containers/RespectButton.js @@ -1,15 +1,15 @@ -import {compose, gql, graphql} from 'react-apollo'; +import {compose, gql} from 'react-apollo'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import get from 'lodash/get'; -import withFragments from 'coral-framework/hocs/withFragments'; +import {withFragments, withMutation} from 'coral-framework/hocs'; import {showSignInDialog} from 'coral-framework/actions/auth'; import RespectButton from '../components/RespectButton'; const isRespectAction = (a) => a.__typename === 'RespectActionSummary'; const COMMENT_FRAGMENT = gql` - fragment RespectButton_updateFragment on Comment { + fragment CoralRespect_UpdateFragment on Comment { action_summaries { ... on RespectActionSummary { count @@ -21,8 +21,8 @@ const COMMENT_FRAGMENT = gql` } `; -const withDeleteAction = graphql(gql` - mutation deleteAction($id: ID!) { +const withDeleteAction = withMutation(gql` + mutation CoralRespect_DeleteAction($id: ID!) { deleteAction(id:$id) { errors { translation_key @@ -66,8 +66,8 @@ const withDeleteAction = graphql(gql` }), }); -const withPostRespect = graphql(gql` - mutation createRespect($respect: CreateRespectInput!) { +const withPostRespect = withMutation(gql` + mutation CoralRespect_CreateRespect($respect: CreateRespectInput!) { createRespect(respect: $respect) { respect { id @@ -137,14 +137,14 @@ const mapDispatchToProps = dispatch => const enhance = compose( withFragments({ root: gql` - fragment RespectButton_root on RootQuery { + fragment CoralRespect_RespectButton_root on RootQuery { me { status } } `, comment: gql` - fragment RespectButton_comment on Comment { + fragment CoralRespect_RespectButton_comment on Comment { action_summaries { ... on RespectActionSummary { count