mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 18:11:40 -05:00
331 lines
12 KiB
JavaScript
331 lines
12 KiB
JavaScript
/*
|
|
|
|
### withMulti
|
|
|
|
Paginated items container
|
|
|
|
Options:
|
|
|
|
- collection: the collection to fetch the documents from
|
|
- fragment: the fragment that defines which properties to fetch
|
|
- fragmentName: the name of the fragment, passed to getFragment
|
|
- limit: the number of documents to show initially
|
|
- pollInterval: how often the data should be updated, in ms (set to 0 to disable polling)
|
|
- terms: an object that defines which documents to fetch
|
|
|
|
Props Received:
|
|
|
|
- terms: an object that defines which documents to fetch
|
|
|
|
Terms object can have the following properties:
|
|
|
|
- view: String
|
|
- userId: String
|
|
- cat: String
|
|
- date: String
|
|
- after: String
|
|
- before: String
|
|
- enableTotal: Boolean
|
|
- enableCache: Boolean
|
|
- listId: String
|
|
- query: String # search query
|
|
- postId: String
|
|
- limit: String
|
|
|
|
*/
|
|
|
|
import React, { Component } from 'react';
|
|
import { withApollo, graphql } from 'react-apollo';
|
|
import gql from 'graphql-tag';
|
|
import update from 'immutability-helper';
|
|
import { getSetting, Utils, multiClientTemplate, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
|
|
import Mingo from 'mingo';
|
|
import compose from 'recompose/compose';
|
|
import withState from 'recompose/withState';
|
|
import find from 'lodash/find';
|
|
|
|
export default function withMulti(options) {
|
|
// console.log(options)
|
|
|
|
let {
|
|
limit = 10,
|
|
pollInterval = getSetting('pollInterval', 20000),
|
|
enableTotal = true,
|
|
enableCache = false,
|
|
extraQueries
|
|
} = options;
|
|
|
|
// if this is the SSR process, set pollInterval to null
|
|
// see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855
|
|
pollInterval = typeof window === 'undefined' ? null : pollInterval;
|
|
|
|
const { collectionName, collection } = extractCollectionInfo(options);
|
|
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
|
|
|
|
const typeName = collection.options.typeName;
|
|
const resolverName = collection.options.multiResolverName;
|
|
|
|
// build graphql query from options
|
|
const query = gql`
|
|
${multiClientTemplate({ typeName, fragmentName, extraQueries })}
|
|
${fragment}
|
|
`;
|
|
|
|
return compose(
|
|
// wrap component with Apollo HoC to give it access to the store
|
|
withApollo,
|
|
|
|
// wrap component with HoC that manages the terms object via its state
|
|
withState('paginationTerms', 'setPaginationTerms', props => {
|
|
// get initial limit from props, or else options
|
|
const paginationLimit = (props.terms && props.terms.limit) || limit;
|
|
const paginationTerms = {
|
|
limit: paginationLimit,
|
|
itemsPerPage: paginationLimit
|
|
};
|
|
|
|
return paginationTerms;
|
|
}),
|
|
|
|
// wrap component with graphql HoC
|
|
graphql(
|
|
query,
|
|
|
|
{
|
|
alias: `with${Utils.pluralize(typeName)}`,
|
|
|
|
// graphql query options
|
|
options({ terms, paginationTerms, client: apolloClient, currentUser }) {
|
|
// get terms from options, then props, then pagination
|
|
const mergedTerms = { ...options.terms, ...terms, ...paginationTerms };
|
|
|
|
const graphQLOptions = {
|
|
variables: {
|
|
input: {
|
|
terms: mergedTerms,
|
|
enableCache,
|
|
enableTotal
|
|
}
|
|
},
|
|
// note: pollInterval can be set to 0 to disable polling (20s by default)
|
|
pollInterval,
|
|
reducer: (previousResults, action) => {
|
|
// see queryReducer function defined below
|
|
return queryReducer(
|
|
typeName,
|
|
previousResults,
|
|
action,
|
|
collection,
|
|
mergedTerms,
|
|
resolverName,
|
|
apolloClient
|
|
);
|
|
}
|
|
};
|
|
|
|
if (options.fetchPolicy) {
|
|
graphQLOptions.fetchPolicy = options.fetchPolicy;
|
|
}
|
|
|
|
// set to true if running into https://github.com/apollographql/apollo-client/issues/1186
|
|
if (options.notifyOnNetworkStatusChange) {
|
|
graphQLOptions.notifyOnNetworkStatusChange = options.notifyOnNetworkStatusChange;
|
|
}
|
|
|
|
return graphQLOptions;
|
|
},
|
|
|
|
// define props returned by graphql HoC
|
|
props(props) {
|
|
// see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts
|
|
const refetch = props.data.refetch,
|
|
// results = Utils.convertDates(collection, props.data[listResolverName]),
|
|
results = props.data[resolverName] && props.data[resolverName].results,
|
|
totalCount = props.data[resolverName] && props.data[resolverName].totalCount,
|
|
networkStatus = props.data.networkStatus,
|
|
loadingInitial = props.data.networkStatus === 1,
|
|
loading = props.data.networkStatus === 1,
|
|
loadingMore = props.data.networkStatus === 2,
|
|
error = props.data.error,
|
|
propertyName = options.propertyName || 'results';
|
|
|
|
if (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(error);
|
|
}
|
|
|
|
return {
|
|
// see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36
|
|
// note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831
|
|
loading,
|
|
loadingInitial,
|
|
loadingMore,
|
|
[propertyName]: results,
|
|
totalCount,
|
|
refetch,
|
|
networkStatus,
|
|
error,
|
|
count: results && results.length,
|
|
|
|
// regular load more (reload everything)
|
|
loadMore(providedTerms) {
|
|
// if new terms are provided by presentational component use them, else default to incrementing current limit once
|
|
const newTerms =
|
|
typeof providedTerms === 'undefined'
|
|
? {
|
|
/*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms,
|
|
limit: results.length + props.ownProps.paginationTerms.itemsPerPage
|
|
}
|
|
: providedTerms;
|
|
|
|
props.ownProps.setPaginationTerms(newTerms);
|
|
},
|
|
|
|
// incremental loading version (only load new content)
|
|
// note: not compatible with polling
|
|
loadMoreInc(providedTerms) {
|
|
// get terms passed as argument or else just default to incrementing the offset
|
|
const newTerms =
|
|
typeof providedTerms === 'undefined'
|
|
? { ...props.ownProps.terms, ...props.ownProps.paginationTerms, offset: results.length }
|
|
: providedTerms;
|
|
|
|
return props.data.fetchMore({
|
|
variables: { input: { terms: newTerms } }, // ??? not sure about 'terms: newTerms'
|
|
updateQuery(previousResults, { fetchMoreResult }) {
|
|
// no more post to fetch
|
|
if (!fetchMoreResult.data) {
|
|
return previousResults;
|
|
}
|
|
const newResults = {};
|
|
newResults[resolverName] = [...previousResults[resolverName], ...fetchMoreResult.data[resolverName]];
|
|
// return the previous results "augmented" with more
|
|
return { ...previousResults, ...newResults };
|
|
}
|
|
});
|
|
},
|
|
|
|
fragmentName,
|
|
fragment,
|
|
...props.ownProps, // pass on the props down to the wrapped component
|
|
data: props.data
|
|
};
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// define query reducer separately
|
|
const queryReducer = (typeName, previousResults, action, collection, mergedTerms, resolverName, apolloClient) => {
|
|
// if collection has no mutations defined, just return previous results
|
|
if (!collection.options.mutations) {
|
|
return previousResults;
|
|
}
|
|
|
|
let newResults = previousResults;
|
|
|
|
// get mongo selector and options objects based on current terms
|
|
const result = collection.getParameters(mergedTerms, apolloClient);
|
|
const { selector, options } = result;
|
|
|
|
const mingoQuery = new Mingo.Query(selector);
|
|
|
|
// function to remove a document from a results object, used by edit and remove cases below
|
|
const removeFromResults = (data, document) => {
|
|
const listWithoutDocument = data[resolverName].results.filter(doc => doc._id !== document._id);
|
|
const currentTotalCount = data[resolverName].totalCount;
|
|
const newResults = update(data, {
|
|
[resolverName]: { $set: { results: listWithoutDocument, totalCount: currentTotalCount - 1 } }
|
|
});
|
|
return newResults;
|
|
};
|
|
|
|
// add document to a results object
|
|
const addToResults = (data, document) => {
|
|
const listWithDocument = [...data[resolverName].results, document];
|
|
const currentTotalCount = data[resolverName].totalCount;
|
|
const newResults = update(data, {
|
|
[resolverName]: { $set: { results: listWithDocument, totalCount: currentTotalCount + 1 } }
|
|
});
|
|
return newResults;
|
|
};
|
|
|
|
// reorder results according to a sort
|
|
const reorderResults = (data, sort) => {
|
|
const list = data[resolverName].results;
|
|
// const convertedList = Utils.convertDates(collection, list); // convert date strings to date objects
|
|
const convertedList = list;
|
|
const cursor = mingoQuery.find(convertedList);
|
|
const sortedList = cursor.sort(sort).all();
|
|
data[resolverName].results = sortedList;
|
|
return data;
|
|
};
|
|
|
|
// console.log('// withList reducer');
|
|
// console.log('terms: ', mergedTerms);
|
|
// console.log('selector: ', selector);
|
|
// console.log('options: ', options);
|
|
// console.log('previousResults: ', previousResults);
|
|
// console.log('action: ', action);
|
|
|
|
switch (action.operationName) {
|
|
case `create${typeName}`:
|
|
// if new document belongs to current list (based on view selector), add it
|
|
const newDocument = action.result.data[`create${typeName}`].data;
|
|
if (mingoQuery.test(newDocument)) {
|
|
if (!find(previousResults[resolverName].results, { _id: newDocument._id })) {
|
|
// make sure it hasn't been already added despite being a create mutation
|
|
// as this reducer may be called several times
|
|
newResults = addToResults(previousResults, newDocument);
|
|
}
|
|
newResults = reorderResults(newResults, options.sort);
|
|
}
|
|
// console.log('** new **')
|
|
// console.log('newDocument: ', newDocument)
|
|
// console.log('belongs to list: ', mingoQuery.test(newDocument))
|
|
break;
|
|
|
|
case `update${typeName}`:
|
|
const editedDocument = action.result.data[`update${typeName}`].data;
|
|
if (mingoQuery.test(editedDocument)) {
|
|
// edited document belongs to the list
|
|
if (!find(previousResults[resolverName].results, { _id: editedDocument._id })) {
|
|
// if document wasn't already in list, add it
|
|
newResults = addToResults(previousResults, editedDocument);
|
|
}
|
|
newResults = reorderResults(newResults, options.sort);
|
|
} else {
|
|
// if edited doesn't belong to current list anymore (based on view selector), remove it
|
|
newResults = removeFromResults(previousResults, editedDocument);
|
|
}
|
|
// console.log('** edit **')
|
|
// console.log('editedDocument: ', editedDocument)
|
|
// console.log('belongs to list: ', mingoQuery.test(editedDocument))
|
|
// console.log('exists in list: ', !!_.findWhere(previousResults[resolverName].results, {_id: editedDocument._id}))
|
|
break;
|
|
|
|
case `delete${typeName}`:
|
|
const removedDocument = action.result.data[`delete${typeName}`].data;
|
|
newResults = removeFromResults(previousResults, removedDocument);
|
|
// console.log('** remove **')
|
|
// console.log('removedDocument: ', removedDocument)
|
|
break;
|
|
|
|
default:
|
|
// console.log('** no action **')
|
|
return previousResults;
|
|
}
|
|
|
|
// console.log('newResults: ', newResults)
|
|
// console.log('\n\n')
|
|
|
|
// copy over arrays explicitely to ensure new sort is taken into account
|
|
return {
|
|
[resolverName]: {
|
|
results: [...newResults[resolverName].results],
|
|
totalCount: newResults[resolverName].totalCount,
|
|
__typename: `Multi${typeName}Output`
|
|
}
|
|
};
|
|
};
|