mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 18:22:23 +08:00
Merge pull request #874 from coralproject/refactor-global-dependencies
Dependency Management
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
import {history} from 'coral-framework/helpers/router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
@@ -47,6 +47,14 @@ const routes = (
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={history} routes={routes}/>;
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
@@ -17,9 +16,9 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
@@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
|
||||
const params = {
|
||||
@@ -27,18 +24,18 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
};
|
||||
}
|
||||
|
||||
return coralApi('/auth/local', params)
|
||||
return rest('/auth/local', params)
|
||||
.then(({user, token}) => {
|
||||
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -84,11 +81,11 @@ const forgotPasswordFailure = (error) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -116,18 +113,18 @@ const checkLoginFailure = (error) => ({
|
||||
error
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return coralApi('/auth')
|
||||
return rest('/auth')
|
||||
.then(({user}) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -136,3 +133,33 @@ export const checkLogin = () => (dispatch) => {
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, {rest, client, storage}) => {
|
||||
return rest('/auth', {method: 'DELETE'}).then(() => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
dispatch({type: 'HANDLE_AUTH_TOKEN'});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
rest(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
@@ -51,15 +50,15 @@ export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
@@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
export const finishInstall = () => (dispatch, getState, {rest}) => {
|
||||
const data = getState().install.data;
|
||||
dispatch(installRequest());
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
return rest('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
@@ -129,9 +128,9 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = (next) => (dispatch) => {
|
||||
export const checkInstall = (next) => (dispatch, _, {rest}) => {
|
||||
dispatch(checkInstallRequest());
|
||||
coralApi('/setup')
|
||||
rest('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
|
||||
@@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => {
|
||||
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
if (storage) {
|
||||
storage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
return {type: actions.HIDE_SHORTCUTS_NOTE};
|
||||
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
|
||||
};
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
@@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch) => {
|
||||
export const fetchSettings = () => (dispatch, _, {rest}) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
coralApi('/settings')
|
||||
rest('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
@@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
export const saveSettingsToServer = () => (dispatch, getState, {rest}) => {
|
||||
let settings = getState().settings;
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
coralApi('/settings', {method: 'PUT', body: settings})
|
||||
rest('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n';
|
||||
*/
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
return (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
@@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => {
|
||||
|
||||
// let a user edit their username
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
return (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
|
||||
@@ -3,12 +3,11 @@ import {connect} from 'react-redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
|
||||
|
||||
@@ -5,22 +5,24 @@ import SuspendUserDialog from '../components/SuspendUserDialog';
|
||||
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
|
||||
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import get from 'lodash/get';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
class SuspendUserDialogContainer extends Component {
|
||||
|
||||
suspendUser = async ({message, until}) => {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props;
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await suspendUser({id: userId, message, until});
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
@@ -33,7 +35,7 @@ class SuspendUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,6 +71,7 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideSuspendUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {add} from 'coral-framework/services/graphqlRegistry';
|
||||
|
||||
const extension = {
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
@@ -11,4 +9,3 @@ const extension = {
|
||||
},
|
||||
};
|
||||
|
||||
add(extension);
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
import smoothscroll from 'smoothscroll-polyfill';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
|
||||
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import {createContext} from 'coral-framework/services/bootstrap';
|
||||
import reducers from './reducers';
|
||||
import App from './components/App';
|
||||
|
||||
import 'react-mdl/extra/material.js';
|
||||
import './graphql';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
import graphqlExtension from './graphql';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
import {toast} from 'react-toastify';
|
||||
import {createNotificationService} from './services/notification';
|
||||
import {hideShortcutsNote} from './actions/moderation';
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
function hidrateStore({store, storage}) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pass redux actions through the emitter.
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
smoothscroll.polyfill();
|
||||
|
||||
const notification = createNotificationService(toast);
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
|
||||
|
||||
// hidrate Store with external data.
|
||||
hidrateStore(context);
|
||||
|
||||
render(
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -5,14 +5,17 @@ const initialState = {
|
||||
modalOpen: false,
|
||||
storySearchVisible: false,
|
||||
storySearchString: '',
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'REVERSE_CHRONOLOGICAL',
|
||||
};
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.MODERATION_CLEAR_STATE:
|
||||
return initialState;
|
||||
return {
|
||||
...initialState,
|
||||
shortcutsNoteVisible: state.shortcutsNoteVisible,
|
||||
};
|
||||
case actions.TOGGLE_MODAL:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -5,17 +5,24 @@ import {Icon} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
const refreshIntervalSeconds = 60 * 5;
|
||||
|
||||
// TODO: refactor out storage code into redux.
|
||||
|
||||
class CountdownTimer extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
storage: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
handleTimeout: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
const {storage} = context;
|
||||
try {
|
||||
if (window.localStorage.getItem('coral:dashboardNote') === null) {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'show');
|
||||
if (storage && storage.getItem('coral:dashboardNote') === null) {
|
||||
storage.setItem('coral:dashboardNote', 'show');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -24,7 +31,7 @@ class CountdownTimer extends React.Component {
|
||||
|
||||
this.state = {
|
||||
secondsUntilRefresh: refreshIntervalSeconds,
|
||||
dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show'
|
||||
dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,8 +61,11 @@ class CountdownTimer extends React.Component {
|
||||
}
|
||||
|
||||
dismissNote = () => {
|
||||
const {storage} = this.context;
|
||||
try {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'hide');
|
||||
if (storage) {
|
||||
storage.setItem('coral:dashboardNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// when setItem fails in Safari Private mode
|
||||
@@ -64,7 +74,8 @@ class CountdownTimer extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' ||
|
||||
const {storage} = this.context;
|
||||
const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') ||
|
||||
this.state.dashboardNote === 'hide'; // for Safari Incognito
|
||||
return (
|
||||
<p
|
||||
|
||||
@@ -26,25 +26,27 @@ import {
|
||||
storySearchChange,
|
||||
clearState
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import Moderation from '../components/Moderation';
|
||||
import Comment from './Comment';
|
||||
import queueConfig from '../queueConfig';
|
||||
import baseQueueConfig from '../queueConfig';
|
||||
|
||||
function prepareNotificationText(text) {
|
||||
return truncate(text, {length: 50}).replace('\n', ' ');
|
||||
}
|
||||
|
||||
function getAssetId(props) {
|
||||
if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) {
|
||||
if (props.params.tabOrId && !(props.params.tabOrId in props.queueConfig)) {
|
||||
return props.params.tabOrId;
|
||||
}
|
||||
return props.params.id || null;
|
||||
}
|
||||
|
||||
function getTab(props) {
|
||||
if (props.params.tabOrId && props.params.tabOrId in queueConfig) {
|
||||
if (props.params.tabOrId && props.params.tabOrId in props.queueConfig) {
|
||||
return props.params.tabOrId;
|
||||
}
|
||||
return props.params.tab || null;
|
||||
@@ -53,8 +55,15 @@ function getTab(props) {
|
||||
class ModerationContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
handleCommentChange = (root, comment, notify) => {
|
||||
return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab);
|
||||
handleCommentChange = (root, comment, notifyText) => {
|
||||
return handleCommentChange(
|
||||
root,
|
||||
comment,
|
||||
this.props.data.variables.sort,
|
||||
() => notifyText && this.props.notify('info', notifyText),
|
||||
this.props.queueConfig,
|
||||
this.activeTab
|
||||
);
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
@@ -78,10 +87,10 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,10 +99,10 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -101,8 +110,8 @@ class ModerationContainer extends Component {
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,8 +120,8 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
const user = comment.actions[comment.actions.length - 1].user;
|
||||
const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -161,8 +170,8 @@ class ModerationContainer extends Component {
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sort: this.props.data.variables.sort,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
statuses: queueConfig[tab].statuses,
|
||||
action_type: queueConfig[tab].action_type,
|
||||
statuses: this.props.queueConfig[tab].statuses,
|
||||
action_type: this.props.queueConfig[tab].action_type,
|
||||
};
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
@@ -206,7 +215,7 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
|
||||
const currentQueueConfig = Object.assign({}, queueConfig);
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
if (premodEnabled) {
|
||||
delete currentQueueConfig.new;
|
||||
} else {
|
||||
@@ -304,7 +313,7 @@ const commentConnectionFragment = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(gql`
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}: comments(query: {
|
||||
@@ -352,7 +361,7 @@ const withModQueueQuery = withQuery(gql`
|
||||
},
|
||||
});
|
||||
|
||||
const withQueueCountPolling = withQuery(gql`
|
||||
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
@@ -393,11 +402,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
clearState,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withQueueCountPolling,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import update from 'immutability-helper';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
|
||||
const limit = 10;
|
||||
|
||||
@@ -92,10 +91,7 @@ function getCommentQueues(comment, queueConfig) {
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sort current sort order of the queues
|
||||
* @param {Object} [notify] show know notifications if set
|
||||
* @param {string} notify.activeQueue current active queue
|
||||
* @param {string} notify.text notification text to show
|
||||
* @param {bool} notify.anyQueue if true show the notification when the comment is shown
|
||||
* @param {string} notify callback to show notification
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @param {Object} queueConfig queue configuration
|
||||
* @return {Object} next state of the store
|
||||
@@ -110,7 +106,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
if (notificationShown) {
|
||||
return;
|
||||
}
|
||||
notification.info(notify);
|
||||
notify();
|
||||
notificationShown = true;
|
||||
};
|
||||
|
||||
@@ -119,13 +115,13 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(next, queue, comment, sort);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
|
||||
showNotificationOnce(comment);
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
} else if(queueHasComment(next, queue, comment.id)){
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
if (notify && activeQueue === queue) {
|
||||
showNotificationOnce(comment);
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +130,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
&& queueHasComment(next, queue, comment.id)
|
||||
&& activeQueue === queue
|
||||
) {
|
||||
showNotificationOnce(comment);
|
||||
showNotificationOnce();
|
||||
}
|
||||
});
|
||||
return next;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* WithQueueConfig takes a `queueConfig` parameter that is
|
||||
* passed down as a prop enriched with queue config data from plugins.
|
||||
*/
|
||||
export default (queueConfig) => hoistStatics((WrappedComponent) => {
|
||||
class WithQueueConfig extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
pluginsConfig = this.context.plugins.getModQueueConfigs();
|
||||
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
queueConfig={{...queueConfig, ...this.pluginsConfig}}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithQueueConfig;
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {getModQueueConfigs} from 'coral-framework/helpers/plugins';
|
||||
|
||||
export default {
|
||||
premod: {
|
||||
@@ -33,5 +32,4 @@ export default {
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.all'),
|
||||
},
|
||||
...getModQueueConfigs(),
|
||||
};
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {toast} from 'react-toastify';
|
||||
|
||||
export function success(msg) {
|
||||
return toast(msg, {type: 'success'});
|
||||
}
|
||||
|
||||
export function error(msg) {
|
||||
return toast(msg, {type: 'error'});
|
||||
}
|
||||
|
||||
export function info(msg) {
|
||||
return toast(msg, {type: 'info'});
|
||||
}
|
||||
|
||||
export function showMutationErrors(error) {
|
||||
console.error(error);
|
||||
if (error.errors) {
|
||||
error.errors.forEach((err) => {
|
||||
toast(
|
||||
err.translation_key ? t(`error.${err.translation_key}`) : err,
|
||||
{type: 'error'}
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* createNotificationService returns a notification services based on toast.
|
||||
* @param {Object} toast
|
||||
* @return {Object} notification service
|
||||
*/
|
||||
export function createNotificationService(toast) {
|
||||
return {
|
||||
success(msg) {
|
||||
toast(msg, {type: 'success'});
|
||||
},
|
||||
error(msg) {
|
||||
toast(msg, {type: 'error'});
|
||||
},
|
||||
info(msg) {
|
||||
toast(msg, {type: 'info'});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(getClient().middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
const coralReducers = {
|
||||
...mainReducer,
|
||||
apollo: getClient().reducer()
|
||||
};
|
||||
|
||||
const store = createStore(
|
||||
combineReducers(coralReducers),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
store.coralReducers = coralReducers;
|
||||
|
||||
window.coralStore = store;
|
||||
export default store;
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
|
||||
import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset';
|
||||
import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Router, Route} from 'react-router';
|
||||
import {history} from 'coral-framework/helpers/router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Embed from './containers/Embed';
|
||||
import {LoginContainer} from 'coral-sign-in/containers/LoginContainer';
|
||||
@@ -12,6 +12,14 @@ const routes = (
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={history} routes={routes} />;
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
|
||||
+7
-8
@@ -1,6 +1,5 @@
|
||||
import * as actions from '../constants/asset';
|
||||
import coralApi from '../helpers/request';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -12,12 +11,12 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R
|
||||
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
|
||||
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
|
||||
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(updateAssetSettingsRequest());
|
||||
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(updateAssetSettingsSuccess(newConfig));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -26,12 +25,12 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(fetchAssetRequest());
|
||||
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
|
||||
rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(fetchAssetSuccess(closedBody));
|
||||
})
|
||||
.catch((error) => {
|
||||
+41
-39
@@ -1,12 +1,8 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import * as Storage from '../helpers/storage';
|
||||
import coralApi, {base} from '../helpers/request';
|
||||
import pym from '../services/pym';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
@@ -59,9 +55,9 @@ export const updateUsername = ({username}) => ({
|
||||
username
|
||||
});
|
||||
|
||||
export const createUsername = (userId, formData) => (dispatch) => {
|
||||
export const createUsername = (userId, formData) => (dispatch, _, {rest}) => {
|
||||
dispatch(createUsernameRequest());
|
||||
coralApi('/account/username', {method: 'PUT', body: formData})
|
||||
rest('/account/username', {method: 'PUT', body: formData})
|
||||
.then(() => {
|
||||
dispatch(createUsernameSuccess());
|
||||
dispatch(hideCreateUsernameDialog());
|
||||
@@ -111,9 +107,11 @@ const signInFailure = (error) => ({
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = (token) => (dispatch) => {
|
||||
Storage.setItem('exp', jwtDecode(token).exp);
|
||||
Storage.setItem('token', token);
|
||||
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch({type: 'HANDLE_AUTH_TOKEN'});
|
||||
};
|
||||
@@ -123,10 +121,10 @@ export const handleAuthToken = (token) => (dispatch) => {
|
||||
//==============================================================================
|
||||
|
||||
export const fetchSignIn = (formData) => {
|
||||
return (dispatch) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch(signInRequest());
|
||||
|
||||
return coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
return rest('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({token}) => {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
dispatch(handleAuthToken(token));
|
||||
@@ -171,10 +169,10 @@ const signInFacebookFailure = (error) => ({
|
||||
error
|
||||
});
|
||||
|
||||
export const fetchSignInFacebook = () => (dispatch) => {
|
||||
export const fetchSignInFacebook = () => (dispatch, _, {rest}) => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
@@ -188,10 +186,10 @@ const signUpFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
|
||||
});
|
||||
|
||||
export const fetchSignUpFacebook = () => (dispatch) => {
|
||||
export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => {
|
||||
dispatch(signUpFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
@@ -220,11 +218,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
|
||||
const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
|
||||
const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error});
|
||||
|
||||
export const fetchSignUp = (formData) => (dispatch, getState) => {
|
||||
export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(signUpRequest());
|
||||
|
||||
coralApi('/users', {
|
||||
rest('/users', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Pym-Url': redirectUri}
|
||||
@@ -256,10 +254,10 @@ const forgotPasswordFailure = (error) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
coralApi('/account/password/reset', {
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: {email, loc: redirectUri}
|
||||
})
|
||||
@@ -275,16 +273,18 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch) => {
|
||||
return coralApi('/auth', {method: 'DELETE'}).then(() => {
|
||||
Storage.removeItem('token');
|
||||
export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => {
|
||||
await rest('/auth', {method: 'DELETE'});
|
||||
|
||||
// Reset the websocket.
|
||||
resetWebsocket();
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
});
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -300,17 +300,19 @@ const checkLoginSuccess = (user, isAdmin) => ({
|
||||
isAdmin
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
|
||||
dispatch(checkLoginRequest());
|
||||
coralApi('/auth')
|
||||
rest('/auth')
|
||||
.then((result) => {
|
||||
if (!result.user) {
|
||||
Storage.removeItem('token');
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
@@ -322,10 +324,10 @@ export const checkLogin = () => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
if (error.status && error.status === 401) {
|
||||
if (error.status && error.status === 401 && storage) {
|
||||
|
||||
// Unauthorized.
|
||||
Storage.removeItem('token');
|
||||
storage.removeItem('token');
|
||||
}
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
@@ -351,10 +353,10 @@ const verifyEmailFailure = () => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = (email) => (dispatch, getState) => {
|
||||
export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(verifyEmailRequest());
|
||||
return coralApi('/users/resend-verify', {
|
||||
return rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: {email},
|
||||
headers: {'X-Pym-Url': redirectUri}
|
||||
@@ -387,11 +389,11 @@ export const setRedirectUri = (uri) => ({
|
||||
const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error});
|
||||
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
|
||||
|
||||
export const editName = (username) => (dispatch) => {
|
||||
return coralApi('/account/username', {method: 'PUT', body: {username}})
|
||||
export const editName = (username) => (dispatch, _, {rest}) => {
|
||||
return rest('/account/username', {method: 'PUT', body: {username}})
|
||||
.then(() => {
|
||||
dispatch(editUsernameSuccess());
|
||||
dispatch(addNotification('success', t('framework.success_name_update')));
|
||||
dispatch(notify('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -1,11 +1,10 @@
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import * as actions from '../constants/stream';
|
||||
import {buildUrl} from 'coral-framework/utils/url';
|
||||
import queryString from 'query-string';
|
||||
|
||||
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
|
||||
|
||||
export const viewAllComments = () => {
|
||||
export const viewAllComments = () => (dispatch, _, {pym}) => {
|
||||
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
@@ -24,10 +23,10 @@ export const viewAllComments = () => {
|
||||
pym.sendMessage('coral-view-all-comments');
|
||||
} catch (e) { /* not sure if we're worried about old browsers */ }
|
||||
|
||||
return {type: actions.VIEW_ALL_COMMENTS};
|
||||
dispatch({type: actions.VIEW_ALL_COMMENTS});
|
||||
};
|
||||
|
||||
export const viewComment = (id) => {
|
||||
export const viewComment = (id) => (dispatch, _, {pym}) => {
|
||||
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
@@ -46,7 +45,7 @@ export const viewComment = (id) => {
|
||||
pym.sendMessage('coral-view-comment', id);
|
||||
} catch (e) { /* not sure if we're worried about old browsers */ }
|
||||
|
||||
return {type: actions.VIEW_COMMENT, id};
|
||||
dispatch({type: actions.VIEW_COMMENT, id});
|
||||
};
|
||||
|
||||
export const addCommentClassName = (className) => ({
|
||||
|
||||
@@ -87,7 +87,7 @@ class AllCommentsPane extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
forEachError(error, ({msg}) => {this.props.notify('error', msg);});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class AllCommentsPane extends React.Component {
|
||||
ignoreUser,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
addNotification,
|
||||
notify,
|
||||
disableReply,
|
||||
postComment,
|
||||
asset,
|
||||
@@ -166,7 +166,7 @@ class AllCommentsPane extends React.Component {
|
||||
disableReply={disableReply}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
depth={0}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
|
||||
@@ -152,7 +152,7 @@ export default class Comment extends React.Component {
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
highlighted: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
liveUpdates: PropTypes.bool,
|
||||
@@ -205,7 +205,7 @@ export default class Comment extends React.Component {
|
||||
onClickEdit (e) {
|
||||
e.preventDefault();
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
this.setState({isEditing: true});
|
||||
@@ -235,7 +235,7 @@ export default class Comment extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
forEachError(error, ({msg}) => {this.props.notify('error', msg);});
|
||||
});
|
||||
emit('ui.Comment.showMoreReplies', {id});
|
||||
return;
|
||||
@@ -252,7 +252,7 @@ export default class Comment extends React.Component {
|
||||
if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.setActiveReplyBox(this.props.comment.id);
|
||||
} else {
|
||||
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ export default class Comment extends React.Component {
|
||||
deleteAction,
|
||||
disableReply,
|
||||
maxCharCount,
|
||||
addNotification,
|
||||
notify,
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
liveUpdates,
|
||||
@@ -482,7 +482,7 @@ export default class Comment extends React.Component {
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
notify={notify} />
|
||||
</span>
|
||||
}
|
||||
{ !isActive &&
|
||||
@@ -494,7 +494,7 @@ export default class Comment extends React.Component {
|
||||
this.state.isEditing
|
||||
? <EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
charCountEnable={charCountEnable}
|
||||
@@ -546,7 +546,7 @@ export default class Comment extends React.Component {
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
@@ -567,7 +567,7 @@ export default class Comment extends React.Component {
|
||||
maxCharCount={maxCharCount}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={(depth < THREADING_LEVEL) ? comment.id : parentId}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
assetId={asset.id}
|
||||
@@ -584,7 +584,7 @@ export default class Comment extends React.Component {
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class EditableCommentContent extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
// show notification to the user (e.g. for errors)
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
|
||||
// comment that is being edited
|
||||
comment: PropTypes.shape({
|
||||
@@ -74,26 +74,26 @@ export class EditableCommentContent extends React.Component {
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loadingState: 'loading'});
|
||||
|
||||
const {editComment, addNotification, stopEditing} = this.props;
|
||||
const {editComment, notify, stopEditing} = this.props;
|
||||
if (typeof editComment !== 'function') {return;}
|
||||
let response;
|
||||
try {
|
||||
response = await editComment({body: this.state.body});
|
||||
this.setState({loadingState: 'success'});
|
||||
const status = response.data.editComment.comment.status;
|
||||
notifyForNewCommentStatus(this.props.addNotification, status);
|
||||
notifyForNewCommentStatus(this.props.notify, status);
|
||||
if (typeof stopEditing === 'function') {
|
||||
stopEditing();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => addNotification('error', msg));
|
||||
forEachError(error, ({msg}) => notify('error', msg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class Stream extends React.Component {
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comment, comments, totalCommentCount}},
|
||||
postComment,
|
||||
addNotification,
|
||||
notify,
|
||||
editComment,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
@@ -147,7 +147,7 @@ class Stream extends React.Component {
|
||||
/>}
|
||||
{showCommentBox &&
|
||||
<CommentBox
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
appendItemArray={appendItemArray}
|
||||
updateItem={updateItem}
|
||||
@@ -185,7 +185,7 @@ class Stream extends React.Component {
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
@@ -241,7 +241,7 @@ class Stream extends React.Component {
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
@@ -270,7 +270,7 @@ class Stream extends React.Component {
|
||||
}
|
||||
|
||||
Stream.propTypes = {
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
|
||||
@@ -18,7 +18,7 @@ export class TopRightMenu extends React.Component {
|
||||
ignoreUser: PropTypes.func,
|
||||
|
||||
// show notification to the user (e.g. for errors)
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -27,7 +27,7 @@ export class TopRightMenu extends React.Component {
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const {comment, ignoreUser, addNotification} = this.props;
|
||||
const {comment, ignoreUser, notify} = this.props;
|
||||
|
||||
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
|
||||
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
|
||||
@@ -40,7 +40,7 @@ export class TopRightMenu extends React.Component {
|
||||
try {
|
||||
await ignoreUser({id});
|
||||
} catch (error) {
|
||||
addNotification('error', 'Failed to ignore user');
|
||||
notify('error', 'Failed to ignore user');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,22 +8,25 @@ import branch from 'recompose/branch';
|
||||
import renderComponent from 'recompose/renderComponent';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import * as authActions from 'coral-framework/actions/auth';
|
||||
import * as assetActions from 'coral-framework/actions/asset';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import * as authActions from '../actions/auth';
|
||||
import * as assetActions from '../actions/asset';
|
||||
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {withQuery} from 'coral-framework/hocs';
|
||||
import Embed from '../components/Embed';
|
||||
import Stream from './Stream';
|
||||
import {addNotification} from 'coral-framework/actions/notification';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import {setActiveTab} from '../actions/embed';
|
||||
|
||||
const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
class EmbedContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
subscriptions = [];
|
||||
|
||||
subscribeToUpdates(props = this.props) {
|
||||
@@ -31,19 +34,19 @@ class EmbedContainer extends React.Component {
|
||||
const newSubscriptions = [{
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
addNotification('info', t('your_account_has_been_banned'));
|
||||
notify('info', t('your_account_has_been_banned'));
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USER_SUSPENDED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
addNotification('info', t('your_account_has_been_suspended'));
|
||||
notify('info', t('your_account_has_been_suspended'));
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
addNotification('info', t('your_username_has_been_rejected'));
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
},
|
||||
}];
|
||||
|
||||
@@ -95,7 +98,7 @@ class EmbedContainer extends React.Component {
|
||||
if (!get(prevProps, 'root.asset.comment') && get(this.props, 'root.asset.comment')) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(() => pym.scrollParentToChildEl('talk-embed-stream-container'), 0);
|
||||
setTimeout(() => this.context.pym.scrollParentToChildEl('talk-embed-stream-container'), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +193,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
checkLogin,
|
||||
setActiveTab,
|
||||
fetchAssetSuccess,
|
||||
addNotification,
|
||||
notify,
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
withDeleteAction, withIgnoreUser, withEditComment
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import * as authActions from 'coral-framework/actions/auth';
|
||||
import * as authActions from 'coral-embed-stream/src/actions/auth';
|
||||
import * as notificationActions from 'coral-framework/actions/notification';
|
||||
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream';
|
||||
import Stream from '../components/Stream';
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '../graphql/utils';
|
||||
|
||||
const {showSignInDialog, editName} = authActions;
|
||||
const {addNotification} = notificationActions;
|
||||
const {notify} = notificationActions;
|
||||
|
||||
class StreamContainer extends React.Component {
|
||||
subscriptions = [];
|
||||
@@ -303,7 +303,7 @@ const mapStateToProps = (state) => ({
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showSignInDialog,
|
||||
addNotification,
|
||||
notify,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
viewAllComments,
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from 'react';
|
||||
import StreamTabPanel from '../components/StreamTabPanel';
|
||||
import {connect} from 'react-redux';
|
||||
import omit from 'lodash/omit';
|
||||
import {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins';
|
||||
import {Tab, TabPane} from 'coral-ui';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class StreamTabPanelContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.fallbackAllTab();
|
||||
@@ -43,28 +45,37 @@ class StreamTabPanelContainer extends React.Component {
|
||||
}
|
||||
|
||||
getSlotComponents(slot, props = this.props) {
|
||||
return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
|
||||
const {plugins} = this.context;
|
||||
return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
|
||||
}
|
||||
|
||||
getPluginTabElements(props = this.props) {
|
||||
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => (
|
||||
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
|
||||
active={this.props.activeTab === PluginComponent.talkPluginName}
|
||||
/>
|
||||
</Tab>
|
||||
));
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return (
|
||||
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
active={this.props.activeTab === PluginComponent.talkPluginName}
|
||||
/>
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => (
|
||||
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
|
||||
/>
|
||||
</TabPane>
|
||||
));
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return (
|
||||
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
/>
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import {add} from 'coral-framework/services/graphqlRegistry';
|
||||
import update from 'immutability-helper';
|
||||
import uuid from 'uuid/v4';
|
||||
import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
|
||||
|
||||
const extension = {
|
||||
export default {
|
||||
fragments: {
|
||||
EditCommentResponse: gql`
|
||||
fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse {
|
||||
@@ -223,4 +222,3 @@ const extension = {
|
||||
},
|
||||
};
|
||||
|
||||
add(extension);
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {checkLogin, handleAuthToken, logout} from 'coral-framework/actions/auth';
|
||||
import './graphql';
|
||||
import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth';
|
||||
import graphqlExtension from './graphql';
|
||||
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
|
||||
import {getStore, injectReducers, addListener} from 'coral-framework/services/store';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import {createReduxEmitter} from 'coral-framework/services/events';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import {createContext} from 'coral-framework/services/bootstrap';
|
||||
import AppRouter from './AppRouter';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
import reducers from './reducers';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
const store = getStore();
|
||||
const client = getClient();
|
||||
const eventEmitter = new EventEmitter({wildcard: true});
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig});
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
injectReducers(reducers);
|
||||
// TODO: move init code into `bootstrap` service after auth has been refactored.
|
||||
const {store, pym} = context;
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
@@ -57,21 +49,11 @@ if (!window.opener) {
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Pass any events through our parent.
|
||||
eventEmitter.onAny((eventName, value) => {
|
||||
pym.sendMessage('event', JSON.stringify({eventName, value}));
|
||||
});
|
||||
|
||||
// Add a redux listener to pass through all actions to our event emitter.
|
||||
addListener(createReduxEmitter(eventEmitter));
|
||||
}
|
||||
|
||||
render(
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
<TalkProvider {...context}>
|
||||
<AppRouter />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import auth from './auth';
|
||||
import asset from './asset';
|
||||
import embed from './embed';
|
||||
import config from './config';
|
||||
import stream from './stream';
|
||||
import {reducer as commentBox} from '../../../talk-plugin-commentbox';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
asset,
|
||||
commentBox,
|
||||
embed,
|
||||
config,
|
||||
stream,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as actions from '../constants/stream';
|
||||
import * as authActions from 'coral-framework/constants/auth';
|
||||
import * as authActions from '../constants/auth';
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
let query = window.location.search.substring(1);
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import pym from '../services/pym';
|
||||
import * as actions from '../constants/notification';
|
||||
|
||||
export const addNotification = (notifType, text) => {
|
||||
pym.sendMessage('coral-alert', `${notifType}|${text}`);
|
||||
return {type: actions.ADD_NOTIFICATION, notifType, text};
|
||||
};
|
||||
export const notify = (kind, msg) => (dispatch, _, {notification}) => {
|
||||
const messages = Array.isArray(msg) ? msg : [msg];
|
||||
|
||||
export const clearNotification = () => {
|
||||
pym.sendMessage('coral-clear-notification');
|
||||
return {type: actions.CLEAR_NOTIFICATION};
|
||||
messages.forEach((message) => {
|
||||
switch (kind) {
|
||||
case 'error':
|
||||
notification.error(message);
|
||||
break;
|
||||
case 'info':
|
||||
notification.info(message);
|
||||
break;
|
||||
case 'success':
|
||||
notification.success(message);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown notification kind ${kind}`);
|
||||
}
|
||||
dispatch({type: actions.NOTIFY, kind, message});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import {Component, cloneElement, Children} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
|
||||
export default class ClickOutside extends Component {
|
||||
static propTypes = {
|
||||
onClickOutside: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
domNode = null;
|
||||
|
||||
handleClick = (e) => {
|
||||
@@ -18,14 +21,20 @@ export default class ClickOutside extends Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {pym} = this.context;
|
||||
this.domNode = findDOMNode(this);
|
||||
document.addEventListener('click', this.handleClick, true);
|
||||
pym.onMessage('click', this.handleClick);
|
||||
if (pym) {
|
||||
pym.onMessage('click', this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const {pym} = this.context;
|
||||
document.removeEventListener('click', this.handleClick, true);
|
||||
pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick);
|
||||
if (pym) {
|
||||
pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
class EventEmitterProvider extends React.Component {
|
||||
getChildContext() {
|
||||
return {eventEmitter: this.props.eventEmitter};
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitterProvider.childContextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EventEmitterProvider;
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
|
||||
import PropTypes from 'prop-types';
|
||||
import omit from 'lodash/omit';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
|
||||
class IfSlotIsEmpty extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
|
||||
@@ -22,7 +24,7 @@ class IfSlotIsEmpty extends React.Component {
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return isSlotEmpty(slot, reduxState, rest);
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
|
||||
import PropTypes from 'prop-types';
|
||||
import omit from 'lodash/omit';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
|
||||
class IfSlotIsNotEmpty extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
|
||||
@@ -22,7 +24,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return isSlotEmpty(slot, reduxState, rest);
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -2,14 +2,18 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './Slot.css';
|
||||
import {connect} from 'react-redux';
|
||||
import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins';
|
||||
import omit from 'lodash/omit';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
|
||||
const emptyConfig = {};
|
||||
|
||||
class Slot extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
@@ -30,15 +34,18 @@ class Slot extends React.Component {
|
||||
}
|
||||
|
||||
getChildren(props = this.props) {
|
||||
return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData);
|
||||
const {plugins} = this.context;
|
||||
return plugins.getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {plugins} = this.context;
|
||||
const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
children = <DefaultComponent {...getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData)} />;
|
||||
const props = plugins.getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData);
|
||||
children = <DefaultComponent {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
class TalkProvider extends React.Component {
|
||||
getChildContext() {
|
||||
return {
|
||||
eventEmitter: this.props.eventEmitter,
|
||||
pym: this.props.pym,
|
||||
plugins: this.props.plugins,
|
||||
rest: this.props.rest,
|
||||
graphqlRegistry: this.props.graphqlRegistry,
|
||||
notification: this.props.notification,
|
||||
storage: this.props.storage,
|
||||
history: this.props.history,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, client, store} = this.props;
|
||||
return (
|
||||
<ApolloProvider client={client} store={store}>
|
||||
{children}
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TalkProvider.childContextTypes = {
|
||||
pym: PropTypes.object,
|
||||
eventEmitter: PropTypes.object,
|
||||
plugins: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
graphqlRegistry: PropTypes.object,
|
||||
notification: PropTypes.object,
|
||||
storage: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TalkProvider;
|
||||
@@ -1,2 +1 @@
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
|
||||
export const NOTIFY = 'NOTIFY';
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import React from 'react';
|
||||
import uniq from 'lodash/uniq';
|
||||
import pick from 'lodash/pick';
|
||||
import merge from 'lodash/merge';
|
||||
import flattenDeep from 'lodash/flattenDeep';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import flatten from 'lodash/flatten';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import {loadTranslations} from 'coral-framework/services/i18n';
|
||||
import {injectReducers} from 'coral-framework/services/store';
|
||||
import {getDisplayName} from 'coral-framework/helpers/hoc';
|
||||
import camelize from './camelize';
|
||||
import plugins from 'pluginsConfig';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
// This is returned for pluginConfig when it is empty.
|
||||
const emptyConfig = {};
|
||||
|
||||
export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
return flatten(plugins
|
||||
|
||||
// Filter out components that have slots and have been disabled in `plugin_config`
|
||||
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
|
||||
|
||||
.filter((o) => o.module.slots[slot])
|
||||
.map((o) => o.module.slots[slot])
|
||||
)
|
||||
.filter((component) => {
|
||||
if(!component.isExcluded) {
|
||||
return true;
|
||||
}
|
||||
let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData);
|
||||
if (component.mapStateToProps) {
|
||||
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
|
||||
}
|
||||
return !component.isExcluded(resolvedProps);
|
||||
});
|
||||
}
|
||||
|
||||
export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
|
||||
return getSlotComponents(slot, reduxState, props, queryData).length === 0;
|
||||
}
|
||||
|
||||
// Memoize the warnings so we only show them once.
|
||||
const memoizedWarnings = [];
|
||||
|
||||
// withWarnings decorates the props of queryData with a proxy that
|
||||
// prints a warning when accessing deeper props.
|
||||
function withWarnings(component, queryData) {
|
||||
if (process.env.NODE_ENV !== 'production' && window.Proxy) {
|
||||
|
||||
// Show warnings when accessing queryData only when not in production.
|
||||
return mapValues(queryData, (value, key) => {
|
||||
|
||||
// Keep null values..
|
||||
if (!queryData[key]) {
|
||||
return queryData[key];
|
||||
}
|
||||
return new Proxy(queryData[key], {
|
||||
get(target, name) {
|
||||
|
||||
// Only care about the components defined in the plugins.
|
||||
if (component.talkPluginName) {
|
||||
const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`;
|
||||
if (memoizedWarnings.indexOf(warning) === -1) {
|
||||
console.warn(warning);
|
||||
memoizedWarnings.push(warning);
|
||||
}
|
||||
}
|
||||
return queryData[key][name];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return queryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* getSlotComponentProps calculate the props we would pass to the slot component.
|
||||
* query datas are only passed to the component if it is defined in `component.fragments`.
|
||||
*/
|
||||
export function getSlotComponentProps(component, reduxState, props, queryData) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
return {
|
||||
...props,
|
||||
config: pluginConfig,
|
||||
...(
|
||||
component.fragments
|
||||
? pick(queryData, Object.keys(component.fragments))
|
||||
: withWarnings(component, queryData)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns React Elements for given slot.
|
||||
*/
|
||||
export function getSlotElements(slot, reduxState, props = {}, queryData = {}) {
|
||||
return getSlotComponents(slot, reduxState, props, queryData)
|
||||
.map((component, i) => {
|
||||
return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)});
|
||||
});
|
||||
}
|
||||
|
||||
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 && fragments[part])
|
||||
.reduce((res, fragments) => {
|
||||
res.push(fragments[part]);
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
export function getGraphQLExtensions() {
|
||||
return plugins
|
||||
.map((o) => pick(o.module, ['mutations', 'queries', 'fragments']))
|
||||
.filter((o) => !isEmpty(o));
|
||||
}
|
||||
|
||||
export function getModQueueConfigs() {
|
||||
return merge(...plugins
|
||||
.map((o) => o.module.modQueues)
|
||||
.filter((o) => o));
|
||||
}
|
||||
|
||||
function getTranslations() {
|
||||
return plugins
|
||||
.map((o) => o.module.translations)
|
||||
.filter((o) => o);
|
||||
}
|
||||
|
||||
export function loadPluginsTranslations() {
|
||||
getTranslations().forEach((t) => loadTranslations(t));
|
||||
}
|
||||
|
||||
export function injectPluginsReducers() {
|
||||
const reducers = merge(
|
||||
...plugins
|
||||
.filter((o) => o.module.reducer)
|
||||
.map((o) => ({[camelize(o.name)] : o.module.reducer}))
|
||||
);
|
||||
injectReducers(reducers);
|
||||
}
|
||||
|
||||
function addMetaDataToSlotComponents() {
|
||||
|
||||
// Add talkPluginName to Slot Components.
|
||||
plugins.forEach((plugin) => {
|
||||
const slots = plugin.module.slots;
|
||||
slots && Object.keys(slots).forEach((slot) => {
|
||||
slots[slot].forEach((component) => {
|
||||
|
||||
// Attach plugin name to the component
|
||||
component.talkPluginName = plugin.name;
|
||||
|
||||
// Attach uuid to the component
|
||||
component.talkUuid = uuid();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addMetaDataToSlotComponents();
|
||||
@@ -1,84 +0,0 @@
|
||||
import bowser from 'bowser';
|
||||
import * as Storage from './storage';
|
||||
import merge from 'lodash/merge';
|
||||
import {getStore} from 'coral-framework/services/store';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
/**
|
||||
* getAuthToken returns the active auth token or null
|
||||
* Note: this method does not have access to the cookie based token used by
|
||||
* browsers that don't allow us to use cross domain iframe local storage.
|
||||
* @return {string|null}
|
||||
*/
|
||||
export const getAuthToken = () => {
|
||||
let state = getStore().getState();
|
||||
|
||||
if (state.config.auth_token) {
|
||||
|
||||
// if an auth_token exists in config, use it.
|
||||
return state.config.auth_token;
|
||||
|
||||
} else if (!bowser.safari && !bowser.ios) {
|
||||
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return Storage.getItem('token');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildOptions = (inputOptions = {}) => {
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
let options = merge({}, defaultOptions, inputOptions);
|
||||
|
||||
// Apply authToken header
|
||||
let authToken = getAuthToken();
|
||||
if (authToken !== null) {
|
||||
options.headers.Authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
if (options.method.toLowerCase() !== 'get') {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleResp = (res) => {
|
||||
if (res.status > 399) {
|
||||
return res.json().then((err) => {
|
||||
let message = err.message || res.status;
|
||||
const error = new Error();
|
||||
|
||||
if (err.error && err.error.metadata && err.error.metadata.message) {
|
||||
error.metadata = err.error.metadata.message;
|
||||
}
|
||||
|
||||
if (err.error && err.error.translation_key) {
|
||||
error.translation_key = err.error.translation_key;
|
||||
}
|
||||
|
||||
error.message = message;
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
});
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
export const base = `${BASE_PATH}api/v1`;
|
||||
|
||||
export default (url, options) => {
|
||||
return fetch(`${base}${url}`, buildOptions(options)).then(handleResp);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import {useBasename} from 'history';
|
||||
import {browserHistory} from 'react-router';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
export const history = useBasename(() => browserHistory)({
|
||||
basename: BASE_PATH
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
let available, error;
|
||||
|
||||
function storageAvailable(type) {
|
||||
let storage = window[type], x = '__storage_test__';
|
||||
try {
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
return (
|
||||
e instanceof DOMException &&
|
||||
|
||||
// everything except Firefox
|
||||
(e.code === 22 ||
|
||||
|
||||
// Firefox
|
||||
|
||||
e.code === 1014 ||
|
||||
|
||||
// test name field too, because code might not be present
|
||||
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
|
||||
|
||||
// acknowledge QuotaExceededError only if there's something already stored
|
||||
storage.length !== 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function lazyCheckStorage() {
|
||||
if (typeof available === 'undefined') {
|
||||
available = storageAvailable('localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
export function getItem(item = '') {
|
||||
lazyCheckStorage();
|
||||
|
||||
if (available) {
|
||||
return localStorage.getItem(item);
|
||||
} else {
|
||||
console.error(
|
||||
`Cannot get from localStorage. localStorage is not available. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function setItem(item = '', value) {
|
||||
lazyCheckStorage();
|
||||
|
||||
if (available) {
|
||||
return localStorage.setItem(item, value);
|
||||
} else {
|
||||
console.error(
|
||||
`Cannot set localStorage. localStorage is not available. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeItem(item = '') {
|
||||
lazyCheckStorage();
|
||||
|
||||
if (available) {
|
||||
return localStorage.removeItem(item);
|
||||
} else {
|
||||
console.error(
|
||||
`Cannot remove item from localStorage. localStorage is not available. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
lazyCheckStorage();
|
||||
|
||||
if (available) {
|
||||
return localStorage.clear();
|
||||
} else {
|
||||
console.error(
|
||||
`Cannot clear localStorage. localStorage is not available. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// window.addEventListener('storage', function(e) {
|
||||
// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`;
|
||||
// console.log(msg);
|
||||
// });
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import graphql from 'graphql-anywhere';
|
||||
import {resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import union from 'lodash/union';
|
||||
|
||||
// TODO: Should not depend on `props.data`
|
||||
@@ -63,7 +63,22 @@ function hasEqualLeaves(a, b, path = '') {
|
||||
|
||||
export default (fragments) => hoistStatics((BaseComponent) => {
|
||||
class WithFragments extends React.Component {
|
||||
fragments = mapValues(fragments, (val) => resolveFragments(val));
|
||||
static contextTypes = {
|
||||
graphqlRegistry: PropTypes.object,
|
||||
};
|
||||
|
||||
get graphqlRegistry() {
|
||||
return this.context.graphqlRegistry;
|
||||
}
|
||||
|
||||
resolveDocument(documentOrCallback) {
|
||||
const document = typeof documentOrCallback === 'function'
|
||||
? documentOrCallback(this.props, this.context)
|
||||
: documentOrCallback;
|
||||
return this.graphqlRegistry.resolveFragments(document);
|
||||
}
|
||||
|
||||
fragments = mapValues(fragments, (val) => this.resolveDocument(val));
|
||||
fragmentKeys = Object.keys(fragments).sort();
|
||||
|
||||
// Cache variables between lifecycles to speed up render.
|
||||
|
||||
@@ -4,7 +4,6 @@ import merge from 'lodash/merge';
|
||||
import uniq from 'lodash/uniq';
|
||||
import flatten from 'lodash/flatten';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import {getDefinitionName, getResponseErrors} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -43,8 +42,20 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
graphqlRegistry: PropTypes.object,
|
||||
};
|
||||
|
||||
get graphqlRegistry() {
|
||||
return this.context.graphqlRegistry;
|
||||
}
|
||||
|
||||
resolveDocument(documentOrCallback) {
|
||||
const document = typeof documentOrCallback === 'function'
|
||||
? documentOrCallback(this.props, this.context)
|
||||
: documentOrCallback;
|
||||
return this.graphqlRegistry.resolveFragments(document);
|
||||
}
|
||||
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
memoized = null;
|
||||
|
||||
@@ -56,7 +67,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
|
||||
propsWrapper = (data) => {
|
||||
const name = getDefinitionName(document);
|
||||
const callbacks = getMutationOptions(name);
|
||||
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()}));
|
||||
@@ -167,7 +178,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
|
||||
getWrapped = () => {
|
||||
if (!this.memoized) {
|
||||
this.memoized = graphql(resolveFragments(document), {...config, props: this.propsWrapper})(WrappedComponent);
|
||||
this.memoized = graphql(this.resolveDocument(document), {...config, props: this.propsWrapper})(WrappedComponent);
|
||||
}
|
||||
return this.memoized;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {graphql} from 'react-apollo';
|
||||
import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
@@ -37,17 +36,28 @@ function networkStatusToString(networkStatus) {
|
||||
* apply query options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
const name = getDefinitionName(document);
|
||||
|
||||
return class WithQuery extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
graphqlRegistry: PropTypes.object,
|
||||
};
|
||||
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
memoized = null;
|
||||
lastNetworkStatus = null;
|
||||
data = null;
|
||||
name = '';
|
||||
|
||||
get graphqlRegistry() {
|
||||
return this.context.graphqlRegistry;
|
||||
}
|
||||
|
||||
resolveDocument(documentOrCallback) {
|
||||
const document = typeof documentOrCallback === 'function'
|
||||
? documentOrCallback(this.props, this.context)
|
||||
: documentOrCallback;
|
||||
return this.graphqlRegistry.resolveFragments(document);
|
||||
}
|
||||
|
||||
emitWhenNeeded(data) {
|
||||
const {variables, networkStatus} = data;
|
||||
@@ -93,11 +103,12 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
refetch: data.refetch,
|
||||
updateQuery: data.updateQuery,
|
||||
subscribeToMore: (stmArgs) => {
|
||||
const resolvedDocument = this.resolveDocument(stmArgs.document);
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolveFragments(stmArgs.document),
|
||||
document: resolvedDocument,
|
||||
onError: (err) => {
|
||||
if (stmArgs.onErr) {
|
||||
return stmArgs.onErr(err);
|
||||
@@ -107,7 +118,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
});
|
||||
},
|
||||
fetchMore: (lmArgs) => {
|
||||
const fetchName = getDefinitionName(lmArgs.query);
|
||||
const resolvedDocument = this.resolveDocument(lmArgs.query);
|
||||
const fetchName = getDefinitionName(resolvedDocument);
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.fetchMore.${fetchName}.begin`,
|
||||
{variables: lmArgs.variables});
|
||||
@@ -115,7 +127,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return data.fetchMore({
|
||||
...lmArgs,
|
||||
query: resolveFragments(lmArgs.query),
|
||||
query: resolvedDocument,
|
||||
})
|
||||
.then((res) => {
|
||||
this.context.eventEmitter.emit(
|
||||
@@ -156,7 +168,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
const base = (typeof this.wrappedConfig.options === 'function')
|
||||
? this.wrappedConfig.options(data)
|
||||
: this.wrappedConfig.options;
|
||||
const configs = getQueryOptions(name);
|
||||
const configs = this.graphqlRegistry.getQueryOptions(name);
|
||||
const reducerCallbacks =
|
||||
[base.reducer || ((i) => i)]
|
||||
.concat(...configs.map((cfg) => cfg.reducer))
|
||||
@@ -178,8 +190,10 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
|
||||
getWrapped = () => {
|
||||
if (!this.memoized) {
|
||||
const resolvedDocument = this.resolveDocument(document);
|
||||
this.name = getDefinitionName(resolvedDocument);
|
||||
this.memoized = graphql(
|
||||
resolveFragments(document),
|
||||
resolvedDocument,
|
||||
{...this.wrappedConfig, options: this.wrappedOptions},
|
||||
)(WrappedComponent);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import auth from './auth';
|
||||
import asset from './asset';
|
||||
import {reducer as commentBox} from '../../talk-plugin-commentbox';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
asset,
|
||||
commentBox,
|
||||
};
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import {createStore} from './store';
|
||||
import {createClient, apolloErrorReporter} from './client';
|
||||
import pym from './pym';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import {createReduxEmitter} from './events';
|
||||
import {createRestClient} from './rest';
|
||||
import thunk from 'redux-thunk';
|
||||
import {loadTranslations} from './i18n';
|
||||
import bowser from 'bowser';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
import {createPluginsService} from './plugins';
|
||||
import {createNotificationService} from './notification';
|
||||
import {createGraphQLRegistry} from './graphqlRegistry';
|
||||
import globalFragments from 'coral-framework/graphql/fragments';
|
||||
import {createStorage} from 'coral-framework/services/storage';
|
||||
import {createHistory} from 'coral-framework/services/history';
|
||||
|
||||
/**
|
||||
* getAuthToken returns the active auth token or null
|
||||
* Note: this method does not have access to the cookie based token used by
|
||||
* browsers that don't allow us to use cross domain iframe local storage.
|
||||
* @return {string|null}
|
||||
*/
|
||||
const getAuthToken = (store, storage) => {
|
||||
let state = store.getState();
|
||||
|
||||
if (state.config.auth_token) {
|
||||
|
||||
// if an auth_token exists in config, use it.
|
||||
return state.config.auth_token;
|
||||
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return storage.getItem('token');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* createContext setups and returns Talk dependencies that should be
|
||||
* passed to `TalkProvider`.
|
||||
* @param {Object} [config] configuration
|
||||
* @param {Object} [config.reducers] extra reducers to add to redux
|
||||
* @param {Array} [config.pluginsConfig] plugin configuration as returned by importing pluginsConfig
|
||||
* @param {Object} [config.graphqlExtensions] additional extension to the graphql framework
|
||||
* @param {Object} [config.notification] replace default notification service
|
||||
* @return {Object} context
|
||||
*/
|
||||
export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) {
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const eventEmitter = new EventEmitter({wildcard: true});
|
||||
const storage = createStorage();
|
||||
const history = createHistory(BASE_PATH);
|
||||
let store = null;
|
||||
const token = () => {
|
||||
|
||||
// Try to get the token from localStorage. If it isn't here, it may
|
||||
// be passed as a cookie.
|
||||
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
|
||||
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
|
||||
return getAuthToken(store, storage);
|
||||
};
|
||||
const rest = createRestClient({
|
||||
uri: `${BASE_PATH}api/v1`,
|
||||
token,
|
||||
});
|
||||
const client = createClient({
|
||||
uri: `${BASE_PATH}api/v1/graph/ql`,
|
||||
liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`,
|
||||
token,
|
||||
});
|
||||
const plugins = createPluginsService(pluginsConfig);
|
||||
const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins));
|
||||
if (!notification) {
|
||||
|
||||
// Use default notification service (pym based)
|
||||
notification = createNotificationService(pym);
|
||||
}
|
||||
const context = {
|
||||
client,
|
||||
pym,
|
||||
plugins,
|
||||
eventEmitter,
|
||||
rest,
|
||||
graphqlRegistry,
|
||||
notification,
|
||||
storage,
|
||||
history,
|
||||
};
|
||||
|
||||
// Load framework fragments.
|
||||
Object.keys(globalFragments).forEach((key) => graphqlRegistry.addFragment(key, globalFragments[key]));
|
||||
|
||||
// Register graphql extension
|
||||
graphqlRegistry.add(graphqlExtension);
|
||||
|
||||
// Register plugin graphql extensions.
|
||||
plugins.getGraphQLExtensions().forEach((ext) => graphqlRegistry.add(ext));
|
||||
|
||||
// Load plugin translations.
|
||||
plugins.getTranslations().forEach((t) => loadTranslations(t));
|
||||
|
||||
// Pass any events through our parent.
|
||||
eventEmitter.onAny((eventName, value) => {
|
||||
pym.sendMessage('event', JSON.stringify({eventName, value}));
|
||||
});
|
||||
|
||||
const finalReducers = {
|
||||
...reducers,
|
||||
...plugins.getReducers(),
|
||||
apollo: client.reducer(),
|
||||
};
|
||||
|
||||
store = createStore(finalReducers, [
|
||||
client.middleware(),
|
||||
thunk.withExtraArgument(context),
|
||||
apolloErrorReporter,
|
||||
createReduxEmitter(eventEmitter),
|
||||
]);
|
||||
|
||||
return {
|
||||
...context,
|
||||
store,
|
||||
};
|
||||
}
|
||||
@@ -1,64 +1,66 @@
|
||||
import ApolloClient, {addTypename, IntrospectionFragmentMatcher} from 'apollo-client';
|
||||
import {networkInterface} from './transport';
|
||||
import ApolloClient, {addTypename, IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client';
|
||||
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
|
||||
import {getAuthToken} from '../helpers/request';
|
||||
import introspectionQueryResultData from '../graphql/introspection.json';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
let client, wsClient = null, wsClientToken = null;
|
||||
|
||||
export function resetWebsocket() {
|
||||
if (wsClient === null) {
|
||||
|
||||
// Nothing to reset!
|
||||
return;
|
||||
// Redux middleware to report any errors to the console.
|
||||
export const apolloErrorReporter = () => (next) => (action) => {
|
||||
if (action.type === 'APOLLO_QUERY_ERROR') {
|
||||
console.error(action.error);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
||||
// Close socket connection which will also unregister subscriptions on the server-side.
|
||||
wsClient.close();
|
||||
|
||||
// Reconnect to the server.
|
||||
wsClient.connect();
|
||||
|
||||
// Reregister all subscriptions (uses non public api).
|
||||
// See: https://github.com/apollographql/subscriptions-transport-ws/issues/171
|
||||
Object.keys(wsClient.operations).forEach((id) => {
|
||||
wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options);
|
||||
});
|
||||
function resolveToken(token) {
|
||||
return typeof token === 'function' ? token() : token;
|
||||
}
|
||||
|
||||
export function getClient(options = {}) {
|
||||
if (client) {
|
||||
return client;
|
||||
}
|
||||
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
wsClient = new SubscriptionClient(`${protocol}://${location.host}${BASE_PATH}api/v1/live`, {
|
||||
/**
|
||||
* createClient setups and returns an Apollo GraphQL Client
|
||||
* @param {Object} [options] configuration
|
||||
* @param {string|function} [options.token] auth token
|
||||
* @param {string} [options.uri] uri of the graphql server
|
||||
* @param {string} [options.liveUri] uri of the graphql subscription server
|
||||
* @return {Object} apollo client
|
||||
*/
|
||||
export function createClient(options = {}) {
|
||||
const {token, uri, liveUri} = options;
|
||||
const wsClient = new SubscriptionClient(liveUri, {
|
||||
reconnect: true,
|
||||
lazy: true,
|
||||
connectionParams: {
|
||||
get token() {
|
||||
|
||||
wsClientToken = getAuthToken();
|
||||
|
||||
// Try to get the token from localStorage. If it isn't here, it may
|
||||
// be passed as a cookie.
|
||||
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
|
||||
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
|
||||
return wsClientToken;
|
||||
}
|
||||
get token() { return resolveToken(token); },
|
||||
}
|
||||
});
|
||||
|
||||
const networkInterface = createNetworkInterface({
|
||||
uri,
|
||||
opts: {
|
||||
credentials: 'same-origin'
|
||||
}
|
||||
});
|
||||
|
||||
networkInterface.use([{
|
||||
applyMiddleware(req, next) {
|
||||
if (!req.options.headers) {
|
||||
req.options.headers = {}; // Create the header object if needed.
|
||||
}
|
||||
|
||||
let authToken = resolveToken(token);
|
||||
if (authToken) {
|
||||
req.options.headers['authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}]);
|
||||
|
||||
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
|
||||
networkInterface,
|
||||
wsClient,
|
||||
);
|
||||
|
||||
client = new ApolloClient({
|
||||
...options,
|
||||
const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}),
|
||||
@@ -72,5 +74,20 @@ export function getClient(options = {}) {
|
||||
networkInterface: networkInterfaceWithSubscriptions,
|
||||
});
|
||||
|
||||
client.resetWebsocket = () => {
|
||||
|
||||
// Close socket connection which will also unregister subscriptions on the server-side.
|
||||
wsClient.close();
|
||||
|
||||
// Reconnect to the server.
|
||||
wsClient.connect();
|
||||
|
||||
// Reregister all subscriptions (uses non public api).
|
||||
// See: https://github.com/apollographql/subscriptions-transport-ws/issues/171
|
||||
Object.keys(wsClient.operations).forEach((id) => {
|
||||
wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options);
|
||||
});
|
||||
};
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/**
|
||||
* createReduxEmitter returns a redux middleware proxying redux actions to
|
||||
* the event emitter
|
||||
* @param {Object} eventEmitter
|
||||
* @return {function} redux middleware
|
||||
*/
|
||||
export function createReduxEmitter(eventEmitter) {
|
||||
return (action) => {
|
||||
return () => (next) => (action) => {
|
||||
|
||||
// Handle apollo actions.
|
||||
if (action.type.startsWith('APOLLO_')) {
|
||||
@@ -7,8 +13,10 @@ export function createReduxEmitter(eventEmitter) {
|
||||
const {operationName, variables, result: {data}} = action;
|
||||
eventEmitter.emit(`subscription.${operationName}.data`, {variables, data});
|
||||
}
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
eventEmitter.emit(`action.${action.type}`, {action});
|
||||
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
|
||||
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';
|
||||
|
||||
@@ -14,272 +12,267 @@ import {gql} from 'react-apollo';
|
||||
*/
|
||||
gql.disableFragmentWarnings();
|
||||
|
||||
const fragments = {};
|
||||
const mutationOptions = {};
|
||||
const queryOptions = {};
|
||||
|
||||
const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value;
|
||||
|
||||
/**
|
||||
* Add fragment
|
||||
*
|
||||
* Example:
|
||||
* addFragment('MyFragment', gql`
|
||||
* fragment Plugin_MyFragment on Comment {
|
||||
* body
|
||||
* }
|
||||
* `);
|
||||
*/
|
||||
export function addFragment(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}`);
|
||||
class GraphQLRegistry {
|
||||
fragments = {};
|
||||
mutationOptions = {};
|
||||
queryOptions = {};
|
||||
|
||||
constructor(getSlotFragments) {
|
||||
this.getSlotFragments = getSlotFragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fragment
|
||||
*
|
||||
* Example:
|
||||
* addFragment('MyFragment', gql`
|
||||
* fragment Plugin_MyFragment on Comment {
|
||||
* body
|
||||
* }
|
||||
* `);
|
||||
*/
|
||||
addFragment(key, document) {
|
||||
const type = getTypeName(document);
|
||||
const name = getDefinitionName(document);
|
||||
if (!(key in this.fragments)) {
|
||||
this.fragments[key] = {type, names: [name], documents: [document]};
|
||||
} else {
|
||||
if (type !== this.fragments[key].type) {
|
||||
console.error(`Type mismatch ${type} !== ${this.fragments[key].type}`);
|
||||
}
|
||||
this.fragments[key].names.push(name);
|
||||
this.fragments[key].documents.push(document);
|
||||
}
|
||||
fragments[key].names.push(name);
|
||||
fragments[key].documents.push(document);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add mutation options.
|
||||
*
|
||||
* Example:
|
||||
* // state is the current redux state, which is sometimes
|
||||
* // necessary to fill the optimistic response.
|
||||
* addMutationOptions('PostComment', ({variables, state}) => ({
|
||||
* optimisticResponse: {
|
||||
* CreateComment: {
|
||||
* extra: '',
|
||||
* },
|
||||
* },
|
||||
* refetchQueries: [],
|
||||
* updateQueries: {
|
||||
* EmbedQuery: (previous, data) => {
|
||||
* return previous;
|
||||
* },
|
||||
* },
|
||||
* update: (proxy, result) => {
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
export function addMutationOptions(key, config) {
|
||||
if (!(key in mutationOptions)) {
|
||||
mutationOptions[key] = [config];
|
||||
} else {
|
||||
mutationOptions[key].push(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query options.
|
||||
*
|
||||
* Example:
|
||||
* addQueryOptions('EmbedQuery', {
|
||||
* reducer: (previousResult, action, variables) => previousResult,
|
||||
* });
|
||||
*/
|
||||
export function addQueryOptions(key, config) {
|
||||
if (!(key in queryOptions)) {
|
||||
queryOptions[key] = [config];
|
||||
} else {
|
||||
queryOptions[key].push(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all fragments, mutation options, and query options defined in the object.
|
||||
*
|
||||
* Example:
|
||||
* add({
|
||||
* fragments: {
|
||||
* CreateCommentResponse: gql`
|
||||
* fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse {
|
||||
* [...]
|
||||
* }`,
|
||||
* },
|
||||
* mutations: {
|
||||
* // state is the current redux state, which is sometimes
|
||||
* // necessary to fill the optimistic response.
|
||||
* PostComment: ({variables, state}) => ({
|
||||
* optimisticResponse: {
|
||||
* [...]
|
||||
* },
|
||||
* refetchQueries: [],
|
||||
* updateQueries: {
|
||||
* EmbedQuery: (previous, data) => {
|
||||
* return previous;
|
||||
* },
|
||||
* },
|
||||
* update: (proxy, result) => {
|
||||
* },
|
||||
* })
|
||||
* },
|
||||
* queries: {
|
||||
* EmbedQuery: {
|
||||
* reducer: (previousResult, action, variables) => {
|
||||
* return previousResult;
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function add(extension) {
|
||||
Object.keys(extension.fragments || []).forEach((key) => addFragment(key, extension.fragments[key]));
|
||||
Object.keys(extension.mutations || []).forEach((key) => addMutationOptions(key, extension.mutations[key]));
|
||||
Object.keys(extension.queries || []).forEach((key) => addQueryOptions(key, extension.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] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* Add mutation options.
|
||||
*
|
||||
* Example:
|
||||
* // state is the current redux state, which is sometimes
|
||||
* // necessary to fill the optimistic response.
|
||||
* addMutationOptions('PostComment', ({variables, state}) => ({
|
||||
* optimisticResponse: {
|
||||
* CreateComment: {
|
||||
* extra: '',
|
||||
* },
|
||||
* },
|
||||
* refetchQueries: [],
|
||||
* updateQueries: {
|
||||
* EmbedQuery: (previous, data) => {
|
||||
* return previous;
|
||||
* },
|
||||
* },
|
||||
* update: (proxy, result) => {
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
addMutationOptions(key, config) {
|
||||
if (!(key in this.mutationOptions)) {
|
||||
this.mutationOptions[key] = [config];
|
||||
} else {
|
||||
this.mutationOptions[key].push(config);
|
||||
}
|
||||
`;
|
||||
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].documents;
|
||||
let fields = `...${fragments[key].names.join('\n...')}\n`;
|
||||
|
||||
// Assemble arguments for `gql` to call it directly without using template literals.
|
||||
const main = `
|
||||
fragment ${key} on ${fragments[key].type} {
|
||||
${fields}
|
||||
/**
|
||||
* Add query options.
|
||||
*
|
||||
* Example:
|
||||
* addQueryOptions('EmbedQuery', {
|
||||
* reducer: (previousResult, action, variables) => previousResult,
|
||||
* });
|
||||
*/
|
||||
addQueryOptions(key, config) {
|
||||
if (!(key in this.queryOptions)) {
|
||||
this.queryOptions[key] = [config];
|
||||
} else {
|
||||
this.queryOptions[key].push(config);
|
||||
}
|
||||
`;
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Add all fragments, mutation options, and query options defined in the object.
|
||||
*
|
||||
* Example:
|
||||
* add({
|
||||
* fragments: {
|
||||
* CreateCommentResponse: gql`
|
||||
* fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse {
|
||||
* [...]
|
||||
* }`,
|
||||
* },
|
||||
* mutations: {
|
||||
* // state is the current redux state, which is sometimes
|
||||
* // necessary to fill the optimistic response.
|
||||
* PostComment: ({variables, state}) => ({
|
||||
* optimisticResponse: {
|
||||
* [...]
|
||||
* },
|
||||
* refetchQueries: [],
|
||||
* updateQueries: {
|
||||
* EmbedQuery: (previous, data) => {
|
||||
* return previous;
|
||||
* },
|
||||
* },
|
||||
* update: (proxy, result) => {
|
||||
* },
|
||||
* })
|
||||
* },
|
||||
* queries: {
|
||||
* EmbedQuery: {
|
||||
* reducer: (previousResult, action, variables) => {
|
||||
* return previousResult;
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
add(extension) {
|
||||
Object.keys(extension.fragments || []).forEach((key) => this.addFragment(key, extension.fragments[key]));
|
||||
Object.keys(extension.mutations || []).forEach((key) => this.addMutationOptions(key, extension.mutations[key]));
|
||||
Object.keys(extension.queries || []).forEach((key) => this.addQueryOptions(key, extension.queries[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;
|
||||
/**
|
||||
* Get a list of mutation options.
|
||||
*/
|
||||
getMutationOptions(key) {
|
||||
return this.mutationOptions[key] || [];
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (initialized) { return; }
|
||||
initialized = true;
|
||||
/**
|
||||
* Get a list of query options.
|
||||
*/
|
||||
getQueryOptions(key) {
|
||||
return this.queryOptions[key] || [];
|
||||
}
|
||||
|
||||
// Add fragments from framework.
|
||||
[globalFragments].forEach((map) =>
|
||||
Object.keys(map).forEach((key) => addFragment(key, map[key]))
|
||||
);
|
||||
|
||||
// Add configs from plugins.
|
||||
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) {
|
||||
|
||||
// Remember keys that we have already resolved.
|
||||
const resolvedKeys = [];
|
||||
|
||||
// Spreads from slots that are empty and need to be removed.
|
||||
// (works around the issue that we don't know the resource type
|
||||
// if we don't have a fragment)
|
||||
const spreadsToBeRemoved = [];
|
||||
|
||||
// fragments to be attached.
|
||||
const subFragments = [];
|
||||
|
||||
// body contains the final result.
|
||||
let body = document.loc.source.body;
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
|
||||
const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || [];
|
||||
uniq(matchedSubFragments.map((f) => f.replace('...', '')))
|
||||
.filter((key) => resolvedKeys.indexOf(key) === -1)
|
||||
.forEach((key) => {
|
||||
const doc = getFragmentDocument(key);
|
||||
if (doc) {
|
||||
subFragments.push(doc);
|
||||
|
||||
// We found a new fragment, so we are not done yet.
|
||||
done = false;
|
||||
} else if(key.startsWith('TalkSlot_')) {
|
||||
spreadsToBeRemoved.push(key);
|
||||
}
|
||||
resolvedKeys.push(key);
|
||||
});
|
||||
|
||||
body = mergeDocuments([body, ...subFragments]).loc.source.body;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
getSlotFragmentDocument(key) {
|
||||
const match = key.match(/TalkSlot_(.*)_(.*)/);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
spreadsToBeRemoved.forEach((key) => {
|
||||
const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g');
|
||||
body = body.replace(regex, '');
|
||||
});
|
||||
const slot = match[1][0].toLowerCase() + match[1].substr(1);
|
||||
const resource = match[2];
|
||||
const documents = this.getSlotFragments(slot, resource);
|
||||
|
||||
return gql`${body}`;
|
||||
} else {
|
||||
console.warn('Can only resolve fragments from documents definied using the gql tag.');
|
||||
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`.
|
||||
*/
|
||||
getRegistryFragmentDocument(key) {
|
||||
if (!(key in this.fragments)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let documents = this.fragments[key].documents;
|
||||
let fields = `...${this.fragments[key].names.join('\n...')}\n`;
|
||||
|
||||
// Assemble arguments for `gql` to call it directly without using template literals.
|
||||
const main = `
|
||||
fragment ${key} on ${this.fragments[key].type} {
|
||||
${fields}
|
||||
}
|
||||
`;
|
||||
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.
|
||||
*/
|
||||
getFragmentDocument(key) {
|
||||
return this.getRegistryFragmentDocument(key) || this.getSlotFragmentDocument(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveFragments finds fragment spread names and attachs
|
||||
* the related fragment document to the given root document.
|
||||
*/
|
||||
resolveFragments(document) {
|
||||
if (document.loc.source) {
|
||||
|
||||
// Remember keys that we have already resolved.
|
||||
const resolvedKeys = [];
|
||||
|
||||
// Spreads from slots that are empty and need to be removed.
|
||||
// (works around the issue that we don't know the resource type
|
||||
// if we don't have a fragment)
|
||||
const spreadsToBeRemoved = [];
|
||||
|
||||
// fragments to be attached.
|
||||
const subFragments = [];
|
||||
|
||||
// body contains the final result.
|
||||
let body = document.loc.source.body;
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
|
||||
const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || [];
|
||||
uniq(matchedSubFragments.map((f) => f.replace('...', '')))
|
||||
.filter((key) => resolvedKeys.indexOf(key) === -1)
|
||||
.forEach((key) => {
|
||||
const doc = this.getFragmentDocument(key);
|
||||
if (doc) {
|
||||
subFragments.push(doc);
|
||||
|
||||
// We found a new fragment, so we are not done yet.
|
||||
done = false;
|
||||
} else if(key.startsWith('TalkSlot_')) {
|
||||
spreadsToBeRemoved.push(key);
|
||||
}
|
||||
resolvedKeys.push(key);
|
||||
});
|
||||
|
||||
body = mergeDocuments([body, ...subFragments]).loc.source.body;
|
||||
}
|
||||
|
||||
spreadsToBeRemoved.forEach((key) => {
|
||||
const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g');
|
||||
body = body.replace(regex, '');
|
||||
});
|
||||
|
||||
return gql`${body}`;
|
||||
} else {
|
||||
console.warn('Can only resolve fragments from documents definied using the gql tag.');
|
||||
}
|
||||
return document;
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* createGraphQLRegistry
|
||||
* @param {Function} getSlotFragments A callback with signature `(slot, part) => [documents]` to retrieve slot fragments.
|
||||
* @return {Object} graphql registry
|
||||
*/
|
||||
export function createGraphQLRegistry(getSlotFragments) {
|
||||
return new GraphQLRegistry(getSlotFragments);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {browserHistory} from 'react-router';
|
||||
import {useBasename} from 'history';
|
||||
|
||||
/**
|
||||
* createHistory returns the history service for react router
|
||||
* @param {string} basename base path of the url
|
||||
* @return {Object} histor service
|
||||
*/
|
||||
export function createHistory(basename) {
|
||||
if (!basename) {
|
||||
return browserHistory;
|
||||
}
|
||||
|
||||
return useBasename(() => browserHistory)({
|
||||
basename
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* createNotificationService returns a notification services based on pym.
|
||||
* @param {Object} pym
|
||||
* @return {Object} notification service
|
||||
*/
|
||||
export function createNotificationService(pym) {
|
||||
return {
|
||||
success(msg) {
|
||||
pym.sendMessage('coral-alert', `success|${msg}`);
|
||||
},
|
||||
error(msg) {
|
||||
pym.sendMessage('coral-alert', `error|${msg}`);
|
||||
},
|
||||
info(msg) {
|
||||
pym.sendMessage('coral-alert', `info|${msg}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import uniq from 'lodash/uniq';
|
||||
import pick from 'lodash/pick';
|
||||
import merge from 'lodash/merge';
|
||||
import flattenDeep from 'lodash/flattenDeep';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import flatten from 'lodash/flatten';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import {getDisplayName} from 'coral-framework/helpers/hoc';
|
||||
import camelize from '../helpers/camelize';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
// This is returned for pluginConfig when it is empty.
|
||||
const emptyConfig = {};
|
||||
|
||||
// Memoize the warnings so we only show them once.
|
||||
const memoizedWarnings = [];
|
||||
|
||||
// withWarnings decorates the props of queryData with a proxy that
|
||||
// prints a warning when accessing deeper props.
|
||||
function withWarnings(component, queryData) {
|
||||
if (process.env.NODE_ENV !== 'production' && window.Proxy) {
|
||||
|
||||
// Show warnings when accessing queryData only when not in production.
|
||||
return mapValues(queryData, (value, key) => {
|
||||
|
||||
// Keep null values..
|
||||
if (!queryData[key]) {
|
||||
return queryData[key];
|
||||
}
|
||||
return new Proxy(queryData[key], {
|
||||
get(target, name) {
|
||||
|
||||
// Detect access from React DevTools and ignore those.
|
||||
const error = new Error();
|
||||
const accessFromDevTools = ['backend.js', 'dehydrate']
|
||||
.every((keyword) => error.stack && error.stack.includes(keyword));
|
||||
|
||||
// Only care about the components defined in the plugins.
|
||||
if (component.talkPluginName && !accessFromDevTools) {
|
||||
const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`;
|
||||
if (memoizedWarnings.indexOf(warning) === -1) {
|
||||
console.warn(warning);
|
||||
memoizedWarnings.push(warning);
|
||||
}
|
||||
}
|
||||
return queryData[key][name];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return queryData;
|
||||
}
|
||||
|
||||
function addMetaDataToSlotComponents(plugins) {
|
||||
|
||||
// Add talkPluginName to Slot Components.
|
||||
plugins.forEach((plugin) => {
|
||||
const slots = plugin.module.slots;
|
||||
slots && Object.keys(slots).forEach((slot) => {
|
||||
slots[slot].forEach((component) => {
|
||||
|
||||
// Attach plugin name to the component
|
||||
component.talkPluginName = plugin.name;
|
||||
|
||||
// Attach uuid to the component
|
||||
component.talkUuid = uuid();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class PluginsService {
|
||||
constructor(plugins) {
|
||||
this.plugins = plugins;
|
||||
addMetaDataToSlotComponents(plugins);
|
||||
}
|
||||
|
||||
getSlotComponents(slot, reduxState, props = {}, queryData = {}) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
return flatten(this.plugins
|
||||
|
||||
// Filter out components that have slots and have been disabled in `plugin_config`
|
||||
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
|
||||
|
||||
.filter((o) => o.module.slots[slot])
|
||||
.map((o) => o.module.slots[slot])
|
||||
)
|
||||
.filter((component) => {
|
||||
if(!component.isExcluded) {
|
||||
return true;
|
||||
}
|
||||
let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData);
|
||||
if (component.mapStateToProps) {
|
||||
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
|
||||
}
|
||||
return !component.isExcluded(resolvedProps);
|
||||
});
|
||||
}
|
||||
|
||||
isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
|
||||
return this.getSlotComponents(slot, reduxState, props, queryData).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* getSlotComponentProps calculate the props we would pass to the slot component.
|
||||
* query datas are only passed to the component if it is defined in `component.fragments`.
|
||||
*/
|
||||
getSlotComponentProps(component, reduxState, props, queryData) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
return {
|
||||
...props,
|
||||
config: pluginConfig,
|
||||
...(
|
||||
component.fragments
|
||||
? pick(queryData, Object.keys(component.fragments))
|
||||
: withWarnings(component, queryData)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns React Elements for given slot.
|
||||
*/
|
||||
getSlotElements(slot, reduxState, props = {}, queryData = {}) {
|
||||
return this.getSlotComponents(slot, reduxState, props, queryData)
|
||||
.map((component, i) => {
|
||||
return React.createElement(component, {key: i, ...this.getSlotComponentProps(component, reduxState, props, queryData)});
|
||||
});
|
||||
}
|
||||
|
||||
getSlotFragments(slot, part) {
|
||||
const components = uniq(flattenDeep(this.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 && fragments[part])
|
||||
.reduce((res, fragments) => {
|
||||
res.push(fragments[part]);
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
getGraphQLExtensions() {
|
||||
return this.plugins
|
||||
.map((o) => pick(o.module, ['mutations', 'queries', 'fragments']))
|
||||
.filter((o) => !isEmpty(o));
|
||||
}
|
||||
|
||||
getModQueueConfigs() {
|
||||
return merge(...this.plugins
|
||||
.map((o) => o.module.modQueues)
|
||||
.filter((o) => o));
|
||||
}
|
||||
|
||||
getTranslations() {
|
||||
return this.plugins
|
||||
.map((o) => o.module.translations)
|
||||
.filter((o) => o);
|
||||
}
|
||||
|
||||
getReducers() {
|
||||
return merge(
|
||||
...this.plugins
|
||||
.filter((o) => o.module.reducer)
|
||||
.map((o) => ({[camelize(o.name)] : o.module.reducer}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createPluginsService returns a plugins service.
|
||||
* @param {Array} plugins config as returned from importing `pluginsConfig`
|
||||
* @return {Object} plugins service
|
||||
*/
|
||||
export function createPluginsService(pluginsConfig) {
|
||||
return new PluginsService(pluginsConfig);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const buildOptions = (inputOptions = {}) => {
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
const options = merge({}, defaultOptions, inputOptions);
|
||||
if (options.method.toLowerCase() !== 'get') {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleResp = (res) => {
|
||||
if (res.status > 399) {
|
||||
return res.json().then((err) => {
|
||||
let message = err.message || res.status;
|
||||
const error = new Error();
|
||||
|
||||
if (err.error && err.error.metadata && err.error.metadata.message) {
|
||||
error.metadata = err.error.metadata.message;
|
||||
}
|
||||
|
||||
if (err.error && err.error.translation_key) {
|
||||
error.translation_key = err.error.translation_key;
|
||||
}
|
||||
|
||||
error.message = message;
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
});
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* createRestClient setups and returns a Rest Client
|
||||
* @param {Object} options configuration
|
||||
* @param {string} options.uri uri of the rest server
|
||||
* @param {string|function} [options.token] auth token
|
||||
* @return {Object} rest client
|
||||
*/
|
||||
export function createRestClient(options) {
|
||||
const {token, uri} = options;
|
||||
const client = (path, options) => {
|
||||
const authToken = typeof token === 'function' ? token() : token;
|
||||
let opts = options;
|
||||
if (authToken) {
|
||||
opts = merge({}, options, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return fetch(`${uri}${path}`, buildOptions(opts)).then(handleResp);
|
||||
};
|
||||
client.uri = uri;
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
function getStorage(type) {
|
||||
let storage = window[type], x = '__storage_test__';
|
||||
try {
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
} catch (e) {
|
||||
const ignore = (
|
||||
e instanceof DOMException &&
|
||||
|
||||
// everything except Firefox
|
||||
(e.code === 22 ||
|
||||
|
||||
// Firefox
|
||||
|
||||
e.code === 1014 ||
|
||||
|
||||
// test name field too, because code might not be present
|
||||
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
|
||||
|
||||
// acknowledge QuotaExceededError only if there's something already stored
|
||||
storage.length !== 0
|
||||
);
|
||||
if (!ignore) {
|
||||
console.warning(e); // eslint-disable-line
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* createStorage returns a localStorage wrapper if available
|
||||
* @return {Object} localStorage wrapper
|
||||
*/
|
||||
export function createStorage() {
|
||||
return getStorage('localStorage');
|
||||
}
|
||||
@@ -1,68 +1,27 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {getClient} from './client';
|
||||
|
||||
let listeners = [];
|
||||
|
||||
export function injectReducers(reducers) {
|
||||
const store = getStore();
|
||||
store.coralReducers = {...store.coralReducers, ...reducers};
|
||||
store.replaceReducer(combineReducers(store.coralReducers));
|
||||
}
|
||||
import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
|
||||
/**
|
||||
* Add a action listener to the redux store.
|
||||
* The action is passed as the first argument to the callback.
|
||||
* createStore creates a Redux Store
|
||||
* @param {Object} reducers addtional reducers
|
||||
* @param {Array} [middlewares] additional middlewares
|
||||
* @return {Object} redux store
|
||||
*/
|
||||
export function addListener(cb) {
|
||||
listeners.push(cb);
|
||||
}
|
||||
|
||||
export function getStore() {
|
||||
if (window.coralStore) {
|
||||
return window.coralStore;
|
||||
}
|
||||
|
||||
const apolloErrorReporter = () => (next) => (action) => {
|
||||
if (action.type === 'APOLLO_QUERY_ERROR') {
|
||||
console.error(action.error);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const customListener = () => (next) => (action) => {
|
||||
listeners.forEach((cb) => {cb(action);});
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
export function createStore(reducers, middlewares = []) {
|
||||
const enhancers = [
|
||||
applyMiddleware(
|
||||
getClient().middleware(),
|
||||
thunk,
|
||||
apolloErrorReporter,
|
||||
customListener,
|
||||
...middlewares,
|
||||
),
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
enhancers.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
const coralReducers = {
|
||||
...mainReducer,
|
||||
apollo: getClient().reducer()
|
||||
};
|
||||
|
||||
window.coralStore = createStore(
|
||||
combineReducers(coralReducers),
|
||||
return reduxCreateStore(
|
||||
combineReducers(reducers),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
compose(...enhancers)
|
||||
);
|
||||
|
||||
window.coralStore.coralReducers = coralReducers;
|
||||
|
||||
return window.coralStore;
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import {createNetworkInterface} from 'apollo-client';
|
||||
import {getAuthToken} from '../helpers/request';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
//==============================================================================
|
||||
// NETWORK INTERFACE
|
||||
//==============================================================================
|
||||
|
||||
const networkInterface = createNetworkInterface({
|
||||
uri: `${BASE_PATH}api/v1/graph/ql`,
|
||||
opts: {
|
||||
credentials: 'same-origin'
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// MIDDLEWARES
|
||||
//==============================================================================
|
||||
|
||||
networkInterface.use([{
|
||||
applyMiddleware(req, next) {
|
||||
if (!req.options.headers) {
|
||||
req.options.headers = {}; // Create the header object if needed.
|
||||
}
|
||||
|
||||
let authToken = getAuthToken();
|
||||
if (authToken) {
|
||||
req.options.headers['authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}]);
|
||||
|
||||
export {
|
||||
networkInterface
|
||||
};
|
||||
@@ -147,6 +147,12 @@ export function forEachError(error, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getErrorMessages(error) {
|
||||
const result = [];
|
||||
forEachError(error, ({msg}) => result.push(msg));
|
||||
return result;
|
||||
}
|
||||
|
||||
const ascending = (a, b) => {
|
||||
const dateA = new Date(a.created_at);
|
||||
const dateB = new Date(b.created_at);
|
||||
|
||||
@@ -11,7 +11,10 @@ import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import IgnoredUsers from '../components/IgnoredUsers';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import CommentHistory from 'talk-plugin-history/CommentHistory';
|
||||
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
|
||||
|
||||
// TODO: Auth logic needs refactoring.
|
||||
import {showSignInDialog, checkLogin} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
|
||||
@@ -12,11 +12,11 @@ export const name = 'talk-plugin-commentbox';
|
||||
|
||||
// Given a newly posted comment's status, show a notification to the user
|
||||
// if needed
|
||||
export const notifyForNewCommentStatus = (addNotification, status) => {
|
||||
export const notifyForNewCommentStatus = (notify, status) => {
|
||||
if (status === 'REJECTED') {
|
||||
addNotification('error', t('comment_box.comment_post_banned_word'));
|
||||
notify('error', t('comment_box.comment_post_banned_word'));
|
||||
} else if (status === 'PREMOD') {
|
||||
addNotification('success', t('comment_box.comment_post_notif_premod'));
|
||||
notify('success', t('comment_box.comment_post_notif_premod'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,12 +45,12 @@ class CommentBox extends React.Component {
|
||||
postComment,
|
||||
assetId,
|
||||
parentId,
|
||||
addNotification,
|
||||
notify,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class CommentBox extends React.Component {
|
||||
// Execute postSubmit Hooks
|
||||
this.state.hooks.postSubmit.forEach((hook) => hook(data));
|
||||
|
||||
notifyForNewCommentStatus(addNotification, postedComment.status);
|
||||
notifyForNewCommentStatus(notify, postedComment.status);
|
||||
|
||||
if (commentPostedHandler) {
|
||||
commentPostedHandler();
|
||||
@@ -81,7 +81,7 @@ class CommentBox extends React.Component {
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(err, ({msg}) => addNotification('error', msg));
|
||||
forEachError(err, ({msg}) => notify('error', msg));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ CommentBox.propTypes = {
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class FlagButton extends Component {
|
||||
this.setState({showMenu: true});
|
||||
}
|
||||
} else {
|
||||
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class CommentHistory extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
forEachError(error, ({msg}) => {this.props.notify('error', msg);});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class ReplyBox extends Component {
|
||||
postComment,
|
||||
assetId,
|
||||
currentUser,
|
||||
addNotification,
|
||||
notify,
|
||||
parentId,
|
||||
commentPostedHandler,
|
||||
maxCharCount,
|
||||
@@ -32,7 +32,7 @@ class ReplyBox extends Component {
|
||||
commentPostedHandler={commentPostedHandler}
|
||||
parentId={parentId}
|
||||
onCancel={this.cancelReply}
|
||||
addNotification={addNotification}
|
||||
notify={notify}
|
||||
currentUser={currentUser}
|
||||
assetId={assetId}
|
||||
postComment={postComment}
|
||||
@@ -47,7 +47,7 @@ ReplyBox.propTypes = {
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import {getDisplayName} from 'coral-framework/helpers/hoc';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import withMutation from 'coral-framework/hocs/withMutation';
|
||||
import {showSignInDialog} from 'coral-framework/actions/auth';
|
||||
import {addNotification} from 'coral-framework/actions/notification';
|
||||
import {capitalize} from 'coral-framework/helpers/strings';
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
@@ -15,6 +14,9 @@ import hoistStatics from 'recompose/hoistStatics';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import {getDefinitionName} from '../utils';
|
||||
|
||||
// TODO: Auth logic needs refactoring.
|
||||
import {showSignInDialog} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
/*
|
||||
* Disable false-positive warning below, as it doesn't work well with how we currently
|
||||
* assemble the queries.
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export {t, timeago} from 'coral-framework/services/i18n';
|
||||
export {can} from 'coral-framework/services/perms';
|
||||
import {isSlotEmpty as ise} from 'coral-framework/helpers/plugins';
|
||||
|
||||
// @TODO: Deprecated.
|
||||
export function isSlotEmpty(...args) {
|
||||
console.warn('A plugin is using `isSlotEmpty` which has been deprecated, please port to the new API using the `IfSlotIsEmpty` and `IfSlotIsNotEmpty` components.');
|
||||
return ise(...args);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ export {
|
||||
insertCommentsSorted,
|
||||
getSlotFragmentSpreads,
|
||||
forEachError,
|
||||
getErrorMessages,
|
||||
getDefinitionName,
|
||||
} from 'coral-framework/utils';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
invalidForm,
|
||||
validForm,
|
||||
createUsername
|
||||
} from 'coral-framework/actions/auth';
|
||||
} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
class ChangeUsernameContainer extends React.Component {
|
||||
constructor(props) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {Button} from 'plugin-api/beta/client/components/ui';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {showSignInDialog} from 'coral-framework/actions/auth';
|
||||
import {showSignInDialog} from 'coral-embed-stream/src/actions/auth';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const SignInButton = ({loggedIn, showSignInDialog}) => (
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
facebookCallback,
|
||||
invalidForm,
|
||||
validForm,
|
||||
} from 'coral-framework/actions/auth';
|
||||
} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
class SignInContainer extends React.Component {
|
||||
constructor(props) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import styles from './styles.css';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {logout} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
|
||||
<div>
|
||||
|
||||
@@ -21,14 +21,14 @@ class ModSubscription extends React.Component {
|
||||
assetId: this.props.data.variables.asset_id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => {
|
||||
const notify = this.props.user.id === user.id
|
||||
const notifyText = this.props.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'talk-plugin-featured-comments.notify_featured',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body),
|
||||
);
|
||||
return this.props.handleCommentChange(prev, comment, notify);
|
||||
return this.props.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user