Files
talk/client/coral-framework/hocs/withMutation.js
T
2018-01-23 20:27:46 +01:00

245 lines
7.5 KiB
JavaScript

import * as React from 'react';
import { graphql } from 'react-apollo';
import merge from 'lodash/merge';
import uniq from 'lodash/uniq';
import flatten from 'lodash/flatten';
import isEmpty from 'lodash/isEmpty';
import {
getDefinitionName,
getResponseErrors,
getErrorMessages,
} from '../utils';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import hoistStatics from 'recompose/hoistStatics';
import union from 'lodash/union';
import { notify } from 'coral-framework/actions/notification';
class ResponseErrors extends Error {
constructor(errors) {
super(`Response Errors ${JSON.stringify(errors)}`);
this.errors = errors.map(e => new ResponseError(e));
}
}
class ResponseError {
constructor(error) {
Object.assign(this, error);
}
translate(...args) {
return t(`error.${this.translation_key}`, ...args);
}
}
const createHOC = (document, config, { notifyOnError = true }) =>
hoistStatics(WrappedComponent => {
config = {
...config,
options: config.options || {},
props: config.props || (data => ({ mutate: data.mutate() })),
};
return class WithMutation extends React.Component {
static contextTypes = {
eventEmitter: PropTypes.object,
store: PropTypes.object,
graphql: PropTypes.object,
};
static propTypes = {
notify: PropTypes.func,
};
get graphqlRegistry() {
return this.context.graphql.registry;
}
notifyErrors(messages) {
this.context.store.dispatch(notify('error', messages));
}
resolveDocument(documentOrCallback) {
return this.context.graphql.resolveDocument(
documentOrCallback,
this.props,
this.context
);
}
// Lazily resolve fragments from graphRegistry to support circular dependencies.
memoized = null;
// Props as we would pass to the BaseComponent without optimizations.
dynamicProps = {};
// Props that are optimized by keeping the identity of function callbacks.
staticProps = {};
propsWrapper = data => {
const name = getDefinitionName(document);
const callbacks = this.graphqlRegistry.getMutationOptions(name);
const mutate = base => {
const variables = base.variables || config.options.variables;
const configs = callbacks.map(cb =>
cb({ variables, state: this.context.store.getState() })
);
const optimisticResponse = merge(
base.optimisticResponse || config.options.optimisticResponse,
...configs.map(cfg => cfg.optimisticResponse)
);
const refetchQueries = flatten(
uniq(
[
base.refetchQueries || config.options.refetchQueries,
...configs.map(cfg => cfg.refetchQueries),
].filter(i => i)
)
);
const updateCallbacks = [base.update || config.options.update]
.concat(...configs.map(cfg => cfg.update))
.filter(i => i);
const update = (proxy, result) => {
if (getResponseErrors(result)) {
// Do not run updates when we have mutation errors.
return;
}
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] = (prev, result) => {
if (getResponseErrors(result.mutationResult)) {
// Do not run updates when we have mutation errors.
return prev;
}
return map[key](prev, result) || prev;
};
} else {
const existing = res[key];
res[key] = (prev, result) => {
if (getResponseErrors(result.mutationResult)) {
// Do not run updates when we have mutation errors.
return prev;
}
const next = existing(prev, result);
return map[key](next, result) || next;
};
}
});
return res;
}, {});
const wrappedConfig = {
variables,
optimisticResponse,
refetchQueries,
updateQueries,
update,
};
if (isEmpty(wrappedConfig.optimisticResponse)) {
delete wrappedConfig.optimisticResponse;
}
this.context.eventEmitter.emit(`mutation.${name}.begin`, {
variables,
});
return data
.mutate(wrappedConfig)
.then(res => {
const errors = getResponseErrors(res);
if (errors) {
throw new ResponseErrors(errors);
}
this.context.eventEmitter.emit(`mutation.${name}.success`, {
variables,
data: res.data,
});
return Promise.resolve(res);
})
.catch(error => {
this.context.eventEmitter.emit(`mutation.${name}.error`, {
variables,
error,
});
// Show errors as notifications.
if (notifyOnError) {
this.notifyErrors(getErrorMessages(error));
}
throw error;
});
};
// Save current props to `dynamicProps`
this.dynamicProps = config.props({ ...data, mutate });
// Sync props to `staticProps`.
// `staticProps` ultimately contains the same props as `dynamicProps` but all callbacks
// keep their identity.
union(
Object.keys(this.dynamicProps),
Object.keys(this.staticProps)
).forEach(key => {
if (!(key in this.dynamicProps)) {
delete this.staticProps[key];
return;
}
if (typeof this.dynamicProps[key] !== 'function') {
this.staticProps[key] = this.dynamicProps[key];
return;
}
if (!(key in this.staticProps)) {
this.staticProps[key] = (...args) =>
this.dynamicProps[key](...args);
return;
}
});
return this.staticProps;
};
getWrapped = () => {
if (!this.memoized) {
this.memoized = graphql(this.resolveDocument(document), {
...config,
props: this.propsWrapper,
})(WrappedComponent);
}
return this.memoized;
};
render() {
const Wrapped = this.getWrapped();
return <Wrapped {...this.props} />;
}
};
});
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply mutation options registered in the graphRegistry.
*
* The returned HOC accepts a settings object with the following properties:
* notifyOnError: show a notification to the user when an error occured.
* Defaults to true.
*/
export default (document, config = {}) => settingsOrComponent => {
if (typeof settingsOrComponent === 'function') {
return createHOC(document, config, {})(settingsOrComponent);
}
return createHOC(document, config, settingsOrComponent);
};