Merge branch 'next' into firefox-actionmenu-deselect

This commit is contained in:
Wyatt Johnson
2018-01-11 20:25:18 -07:00
678 changed files with 16487 additions and 13094 deletions
+17 -17
View File
@@ -1,44 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
const routes = (
<div>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="stories" component={Stories} />
{/* Community Routes */}
<Route path='community'>
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
<Route path="community">
<Route path="flagged" components={Community}>
<Route path=":id" components={Community} />
</Route>
<Route path='people' components={Community}>
<Route path=':id' components={Community} />
<Route path="people" components={Community}>
<Route path=":id" components={Community} />
</Route>
<IndexRedirect to='flagged' />
<IndexRedirect to="flagged" />
</Route>
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path="moderate" component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path=':tabOrId' components={Moderation} />
<Route path=":tabOrId" components={Moderation} />
<Route path=':tab' components={Moderation}>
<Route path=':id' components={Moderation} />
<Route path=":tab" components={Moderation}>
<Route path=":id" components={Moderation} />
</Route>
</Route>
</Route>
+43 -34
View File
@@ -7,26 +7,29 @@ import jwtDecode from 'jwt-decode';
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
dispatch({type: actions.LOGIN_REQUEST});
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, storage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password
}
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({user, token}) => {
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -38,19 +41,19 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password')
message: t('error.email_password'),
});
}
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
@@ -69,27 +72,32 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = (error) => ({
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/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) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
@@ -99,24 +107,24 @@ export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
isAdmin,
});
const checkLoginFailure = (error) => ({
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error
error,
});
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({user}) => {
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -127,9 +135,11 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
@@ -138,8 +148,8 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, {rest, client, storage}) => {
return rest('/auth', {method: 'DELETE'}).then(() => {
export const logout = () => (dispatch, _, { rest, client, storage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (storage) {
storage.removeItem('token');
}
@@ -147,7 +157,7 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
dispatch({ type: actions.LOGOUT });
});
};
@@ -155,11 +165,10 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
export const handleAuthToken = token => (dispatch, _, { storage }) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
@@ -1,6 +1,19 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
export const showBanUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_BAN_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
+23 -16
View File
@@ -10,54 +10,61 @@ import {
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
import t from 'coral-framework/services/i18n';
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
dispatch(requestFetchUsers());
rest(`/users?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
.then(({ result, page, count, limit, totalPages }) => {
dispatch({
type: FETCH_USERS_SUCCESS,
users: result,
page,
count,
limit,
totalPages
totalPages,
});
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
});
};
const requestFetchUsers = () => ({
type: FETCH_USERS_REQUEST
type: FETCH_USERS_REQUEST,
});
export const updateSorting = (sort) => ({
export const updateSorting = sort => ({
type: SORT_UPDATE,
sort
sort,
});
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Reject Username Dialog
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
user,
});
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
+2 -2
View File
@@ -1,7 +1,7 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => (dispatch) => {
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
dispatch({ type: CONFIG_UPDATED, data });
};
+5 -5
View File
@@ -1,13 +1,13 @@
import * as actions from 'constants/configure';
export const updatePending = ({updater, errorUpdater}) => {
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
export const updatePending = ({ updater, errorUpdater }) => {
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
};
export const clearPending = () => {
return {type: actions.CLEAR_PENDING};
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = (section) => {
return {type: actions.SET_ACTIVE_SECTION, section};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
};
+77 -60
View File
@@ -3,17 +3,17 @@ import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import t from 'coral-framework/services/i18n';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
export const nextStep = () => ({ type: actions.NEXT_STEP });
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = (error) => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
const hasError = error => ({ type: actions.HAS_ERROR, error });
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
@@ -21,24 +21,21 @@ const validation = (formData, dispatch, next) => {
return;
}
const validKeys = Object.keys(formData)
.filter((name) => name !== 'domains');
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
const empty = validKeys.filter(name => {
const cond = !formData[name].length;
if (cond) {
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (empty.length) {
dispatch(hasError());
@@ -46,19 +43,17 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
const validation = validKeys
.filter((name) => {
const cond = !validate[name](formData[name]);
if (cond) {
const validation = validKeys.filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (validation.length) {
dispatch(hasError());
@@ -67,20 +62,21 @@ const validation = (formData, dispatch, next) => {
// Confirm Validation
const prefixLength = 'confirm'.length;
const confirm = validKeys
.filter((name) => {
if (!name.startsWith('confirm')) {
return false;
}
const confirm = validKeys.filter(name => {
if (!name.startsWith('confirm')) {
return false;
}
// Check that 'confirmX' equals 'X'.
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
// Check that 'confirmX' equals 'X'.
const other =
name.substr(prefixLength, 1).toLowerCase() +
name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
if (confirm.length) {
dispatch(hasError());
@@ -105,41 +101,62 @@ export const submitUser = () => (dispatch, getState) => {
});
};
export const finishInstall = () => (dispatch, getState, {rest}) => {
export const finishInstall = () => (dispatch, getState, { rest }) => {
const data = getState().install.data;
dispatch(installRequest());
return rest('/setup', {method: 'POST', body: data})
return rest('/setup', { method: 'POST', body: data })
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(installFailure(errorMessage));
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
export const updateSettingsFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_SETTINGS,
name,
value,
});
export const updateUserFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_USER,
name,
value,
});
export const updatePermittedDomains = value => ({
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
value,
});
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});
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) => async (dispatch, _, {rest}) => {
export const checkInstall = next => async (dispatch, _, { rest }) => {
dispatch(checkInstallRequest());
try {
const {installed} = await rest('/setup');
const { installed } = await rest('/setup');
dispatch(checkInstallSuccess(installed));
if (installed) {
next();
}
} catch (error) {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkInstallFailure(errorMessage));
}
};
+12 -13
View File
@@ -1,41 +1,40 @@
import * as actions from 'constants/moderation';
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
export const singleView = () => ({ type: actions.SINGLE_VIEW });
// hide shortcuts note
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
try {
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
}
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
};
export const setSortOrder = (order) => ({
export const setSortOrder = order => ({
type: actions.SET_SORT_ORDER,
order
order,
});
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
export const toggleStorySearch = active => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
});
export const storySearchChange = (value) => ({
export const storySearchChange = value => ({
type: actions.STORY_SEARCH_CHANGE_VALUE,
value
value,
});
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
type: actions.MODERATION_CLEAR_STATE,
});
export const selectCommentId = (id) => ({
export const selectCommentId = id => ({
type: actions.MODERATION_SELECT_COMMENT,
id,
});
+27 -22
View File
@@ -10,7 +10,7 @@ import {
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,53 +21,58 @@ 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 = (query = {}) => (dispatch, _, {rest}) => {
dispatch({type: FETCH_ASSETS_REQUEST});
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
.then(({ result, page, count, limit, totalPages }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
}))
.catch((error) => {
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
});
};
export const updateAssets = (assets) => (dispatch) => {
dispatch({type: UPDATE_ASSETS, assets});
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
export const setCriteria = (criteria) => ({
export const setCriteria = criteria => ({
type: SET_CRITERIA,
criteria,
});
@@ -1,7 +1,19 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog.js';
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
export const showSuspendUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_SUSPEND_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
+18 -9
View File
@@ -1,28 +1,37 @@
import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const viewUserDetail = userId => ({
type: actions.VIEW_USER_DETAIL,
userId,
});
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
export const changeTab = (tab) => {
export const changeTab = tab => {
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses};
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const clearUserDetailSelections = () => ({
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
type: active
? actions.SELECT_USER_DETAIL_COMMENT
: actions.UNSELECT_USER_DETAIL_COMMENT,
id,
};
};
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
return {
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
ids
type: active
? actions.SELECT_ALL_USER_DETAIL_COMMENT
: actions.CLEAR_USER_DETAIL_SELECTIONS,
ids,
};
};
+39 -21
View File
@@ -6,38 +6,56 @@ import t from 'coral-framework/services/i18n';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch, _, {rest}) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => {
return (dispatch, _, { rest }) => {
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
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);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.UPDATE_STATUS_FAILURE,
error: errorMessage,
});
});
};
};
// change status of a user
export const sendNotificationEmail = (userId, 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();
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
});
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();
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
});
};
};
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => {
export const enableUsernameEdit = userId => {
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();
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.USERNAME_ENABLE_FAILURE,
error: errorMessage,
});
}
);
};
};
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {murmur3} from 'murmurhash-js';
import { murmur3 } from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
@@ -8,48 +8,88 @@ import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(flatten(Object.keys(userState.status)
.filter((k) => k !== '__typename')
.map((k) => userState.status[k].history)), 'created_at', 'desc');
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.map(k => userState.status[k].history)
),
'created_at',
'desc'
);
};
const buildActionResponse = (typename, status) => {
const actionResponses = {
'UsernameStatusHistory' : `Username Status: ${status}`,
'BannedStatusHistory': status ? 'User banned' : 'Ban removed',
'SuspensionStatusHistory': status ? 'Account Suspended' : 'Suspension removed'
};
const buildActionResponse = (typename, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
default:
return '-';
}
};
return actionResponses[typename];
const getModerationValue = (userId, assignedBy = {}) => {
if (assignedBy && userId !== assignedBy.id) {
return assignedBy.username;
}
return 'SYSTEM';
};
class AccountHistory extends React.Component {
render() {
const {userState} = this.props;
const userHistory = buildUserHistory(userState);
const { user } = this.props;
const userHistory = buildUserHistory(user.state);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div className={cn(styles.headerRow, 'talk-admin-account-history-header-row')}>
<div
className={cn(
styles.headerRow,
'talk-admin-account-history-header-row'
)}
>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{
userHistory.map((h) => (
<div className={cn(styles.row, 'talk-admin-account-history-row')} key={`${h.__typename}_${murmur3(h.created_at)}`}>
<div className={cn(styles.item, 'talk-admin-account-history-row-date')}>
{moment(new Date(h.created_at)).format('MMM DD, YYYY')}
{userHistory.map(
({ __typename, created_at, assigned_by, until, status }) => (
<div
className={cn(styles.row, 'talk-admin-account-history-row')}
key={`${__typename}_${murmur3(created_at)}`}
>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-date'
)}
>
{moment(new Date(created_at)).format('MMM DD, YYYY')}
</div>
<div className={cn(styles.item, styles.action, 'talk-admin-account-history-row-status')}>
{buildActionResponse(h.__typename, h.status)}
<div
className={cn(
styles.item,
styles.action,
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
</div>
<div className={cn(styles.item, 'talk-admin-account-history-row-assigned-by')}>
{h.assigned_by ? h.assigned_by.username : 'SYSTEM'}
<div
className={cn(
styles.item,
'talk-admin-account-history-row-assigned-by'
)}
>
{getModerationValue(user.id, assigned_by)}
</div>
</div>
))
}
)
)}
</div>
</div>
);
@@ -57,8 +97,7 @@ class AccountHistory extends React.Component {
}
AccountHistory.propTypes = {
history: PropTypes.array,
userState: PropTypes.object,
user: PropTypes.object.isRequired,
};
export default AccountHistory;
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import { Button, Icon } from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {Menu} from 'react-mdl';
import { Menu } from 'react-mdl';
import cn from 'classnames';
import {findDOMNode} from 'react-dom';
import { findDOMNode } from 'react-dom';
import styles from './ActionsMenu.css';
import t from 'coral-framework/services/i18n';
@@ -14,40 +14,47 @@ let count = 0;
class ActionsMenu extends React.Component {
id = `actions-dropdown-${count++}`;
menu = null;
state = {open: false};
state = { open: false };
timeout = null;
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleRef = (ref) => {
handleRef = ref => {
this.menu = ref ? findDOMNode(ref).parentNode : null;
}
};
syncOpenState = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
}, 150);
};
render() {
const {className = '', buttonClassNames = '', label = ''} = this.props;
const { className = '', buttonClassNames = '', label = '' } = this.props;
return (
<ClickOutside onClickOutside={this.syncOpenState}>
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<div className={cn(styles.root, className)} onBlur={this.syncOpenState}>
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open}, buttonClassNames)}
cStyle="actions"
className={cn(
styles.button,
{ [styles.buttonOpen]: this.state.open },
buttonClassNames
)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
raised
>
{label ? label : t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
name={
this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
}
className={styles.arrowIcon}
/>
</Button>
@@ -1,13 +1,18 @@
import React from 'react';
import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import { MenuItem } from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
const ActionsMenuItem = props => (
<MenuItem
className={cn(styles.menuItem, props.className, 'action-menu-item')}
{...props}
id={camelCase(props.children)}
/>
);
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
+80 -54
View File
@@ -2,102 +2,128 @@ import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
class AdminLogin extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
this.state = { email: '', password: '', requestPassword: false };
}
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
};
onRecaptchaLoad = () => {
// do something?
}
};
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
onRecaptchaVerify = recaptchaResponse => {
this.props.handleLogin(
this.state.email,
this.state.password,
recaptchaResponse
);
};
handleRequestPassword = (e) => {
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
};
render () {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
render() {
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
const signInForm = (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label='Email Address'
label="Email Address"
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
onChange={e => this.setState({ email: e.target.value })}
/>
<TextField
id="password"
label='Password'
label="Password"
value={this.state.password}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
onChange={e => this.setState({ password: e.target.value })}
type="password"
/>
<div style={{ height: 10 }} />
<Button
className="talk-admin-login-sign-in-button"
type='submit'
cStyle='black'
type="submit"
cStyle="black"
full
onClick={this.handleSignIn}>Sign In</Button>
onClick={this.handleSignIn}
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={e => {
e.preventDefault();
this.setState({ requestPassword: true });
}}
>
Request a new one.
</a>
</p>
{
loginMaxExceeded &&
{loginMaxExceeded && (
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
render="explicit"
theme="dark"
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
verifyCallback={this.onRecaptchaVerify}
/>
)}
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
const requestPasswordForm = this.props.passwordRequestSuccess ? (
<p
className={styles.passwordRequestSuccess}
onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
}}
>
{this.props.passwordRequestSuccess}{' '}
<a className={styles.signInLink} href="#">
Sign in
</a>
<Success />
</p>
) : (
<form onSubmit={this.handleRequestPassword}>
<TextField
label="Email Address"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>
<Button
type="submit"
cStyle="black"
full
onClick={this.handleRequestPassword}
>
Reset Password
</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
<p className={styles.loginCTA}>
Sign in to interact with your community.
</p>
{this.state.requestPassword ? requestPasswordForm : signInForm}
</div>
</Layout>
);
+1 -1
View File
@@ -5,7 +5,7 @@ import 'material-design-lite';
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render () {
render() {
return (
<div>
<ToastContainer />
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick, className}) => {
const ApproveButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -28,4 +32,3 @@ ApproveButton.propTypes = {
};
export default ApproveButton;
@@ -1,16 +1,15 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const initialState = {step: 0, message: ''};
const initialState = { step: 0, message: '' };
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -18,53 +17,44 @@ class BanUserDialog extends React.Component {
this.setState(initialState);
}
}
handleMessageChange = (e) => {
const {value: message} = e;
this.setState({message});
}
handleMessageChange = e => {
const { value: message } = e;
this.setState({ message });
};
goToStep1 = () => {
this.setState({
step: 1,
message: t(
'bandialog.email_message_ban',
this.props.username,
),
message: t('bandialog.email_message_ban', this.props.username),
});
}
};
renderStep0() {
const {
onCancel,
username,
info,
} = this.props;
const { onCancel, username, info } = this.props;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.ban_user')}
</h2>
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>
{info}
</p>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised >
raised
>
{t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -73,22 +63,19 @@ class BanUserDialog extends React.Component {
}
renderStep1() {
const {
onCancel,
onPerform,
} = this.props;
const {message} = this.state;
const { onCancel, onPerform } = this.props;
const { message } = this.state;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.notify_ban_headline')}
</h2>
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>{t('bandialog.write_a_message')}</legend>
<legend className={styles.legend}>
{t('bandialog.write_a_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
@@ -101,14 +88,16 @@ class BanUserDialog extends React.Component {
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
raised
>
{t('bandialog.send')}
</Button>
</div>
@@ -117,16 +106,19 @@ class BanUserDialog extends React.Component {
}
render() {
const {step} = this.state;
const {open, onCancel} = this.props;
const { step } = this.state;
const { open, onCancel } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')} >
<span className={styles.close} onClick={onCancel}>×</span>
title={t('bandialog.ban_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
@@ -1,15 +1,11 @@
import React from 'react';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
import { withCopyToClipboard } from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
render() {
return <Button {...this.props}>{t('common.copy')}</Button>;
}
}
@@ -1,10 +1,10 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import { murmur3 } from 'murmurhash-js';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({children, body}) => {
const CommentBodyHighlighter = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -20,7 +20,9 @@ const CommentBodyHighlighter = ({children, body}) => {
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
{React.cloneElement(React.Children.only(children), {
key: murmur3(body),
})}
</CSSTransitionGroup>
);
};
@@ -1,5 +1,5 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
@@ -9,18 +9,18 @@ function escapeRegExp(string) {
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
@@ -39,10 +39,9 @@ const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
return tokens.map(
(token, i) =>
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
);
}
@@ -53,34 +52,26 @@ function markLinks(body) {
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<div {...rest}>
{content}
</div>
);
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
};
@@ -5,7 +5,7 @@
.moreDetail {
position: absolute;
font-size: 12px;
font-size: 13px;
font-weight: 500;
color: black;
right: 16px;
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
@@ -7,26 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
showDetail: false,
};
constructor () {
constructor() {
super();
this.state = {
showDetail: false
showDetail: false,
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
this.setState(state => ({
showDetail: !state.showDetail,
}));
this.props.clearHeightCache && this.props.clearHeightCache();
}
};
render() {
const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const { data, root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
root,
comment,
@@ -49,12 +49,14 @@ class CommentDetails extends Component {
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>}
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
)}
</div>
);
}
@@ -9,37 +9,67 @@ import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
return actions.some(
action => action.__typename === 'FlagAction' && action.user
);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
return actions.some(
action =>
action.__typename === 'FlagAction' &&
action.user &&
staffRoles.includes(action.user.role)
) ? 'Staff' : 'User';
)
? 'Staff'
: 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
);
}
function hasHistoryFlag(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
return actions.some(
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
);
}
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
{hasParent && (
<Label iconName="reply" className={styles.replyLabel}>
reply
</Label>
)}
{status === 'PREMOD' && (
<Label iconName="query_builder" className={styles.premodLabel}>
Pre-Mod
</Label>
)}
{isUserFlagged(actions) && (
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
)}
{hasSuspectedWords(actions) && (
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
)}
{hasHistoryFlag(actions) && (
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
)}
</div>
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
/>
</div>
);
};
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
import t from 'coral-framework/services/i18n';
const CountBadge = ({count}) => {
const CountBadge = ({ count }) => {
let number = count;
// shorten large counts to abbreviations
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
return <span className={styles.count}>{number}</span>;
};
CountBadge.propTypes = {
count: PropTypes.number.isRequired
count: PropTypes.number.isRequired,
};
export default CountBadge;
+30 -30
View File
@@ -1,61 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import { Navigation, Drawer } from 'react-mdl';
import { IndexLink, Link } from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
const CoralDrawer = ({ handleLogout, auth = {} }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</IndexLink>
)
}
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
</IndexLink>
)}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div> : null }
</div>
) : null}
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object
auth: PropTypes.object,
};
export default CoralDrawer;
@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import { Card } from 'coral-ui';
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
const EmptyCard = props => (
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
{props.children}
</Card>
);
EmptyCard.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
export default EmptyCard;
@@ -1,11 +1,11 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Loading</h1>
<CoralLogo />
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
export default ({text, children}) => {
export default ({ text, children }) => {
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
@@ -1,24 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className = '', ...rest}) =>
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={loadMore}>
{showLoadMore && (
<Button className={styles.loadMore} onClick={loadMore}>
Load More
</Button>
}
</div>;
)}
</div>
);
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
showLoadMore: PropTypes.bool.isRequired,
};
export default LoadMore;
+5 -3
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import { Button, Icon } from 'react-mdl';
import styles from './Modal.css';
export default ({open, children, onClose}) => (
export default ({ open, children, onClose }) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
<Button className={styles.close} onClick={onClose}>
<Icon name="close" />
</Button>
{children}
</div>
</div>
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
queueCount: PropTypes.number.isRequired,
};
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
j: 'modqueue.next_comment',
k: 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
s: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
d: 'modqueue.approve',
f: 'modqueue.reject',
},
},
];
}
};
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
render() {
const {
open,
onClose,
hideShortcutsNote,
shortcutsNoteVisible,
} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<div
className={styles.callToAction}
style={{
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
}}
>
<div onClick={hideShortcutsNote} className={styles.closeButton}>
×
</div>
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
<p><strong>{t('modqueue.try_these')}:</strong></p>
<p>
<strong>{t('modqueue.try_these')}:</strong>
</p>
<ul>
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
<li>
<span>{t('modqueue.approve')}</span>{' '}
<span className={styles.smallKey}>d</span>
</li>
<li>
<span>{t('modqueue.reject')}</span>{' '}
<span className={styles.smallKey}>f</span>
</li>
</ul>
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
<p>
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
<span className={styles.smallKey}>/</span>
</p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map((key) => (
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td className={styles.shortcut}>
<span className={styles.key}>{key}</span>
</td>
<td>{t(shortcut.shortcuts[key])}</td>
</tr>
))}
@@ -1,13 +1,16 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
<img
src="https://coralproject.net/images/communicorn.jpg"
alt="Communicorn"
/>
</div>
</Layout>
);
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick, className}) => {
const RejectButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -28,4 +32,3 @@ RejectButton.propTypes = {
};
export default RejectButton;
@@ -1,25 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import { Dialog } from 'coral-ui';
import { RadioGroup, Radio } from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
import t, {timeago} from 'coral-framework/services/i18n';
import {dateAdd} from 'coral-framework/utils';
import t, { timeago } from 'coral-framework/services/i18n';
import { dateAdd } from 'coral-framework/utils';
const initialState = {step: 0, duration: '3'};
const initialState = { step: 0, duration: '3' };
function durationsToDate(hours) {
// Add 1 minute more to help `timeago.js` to display the correct duration.
return dateAdd(new Date(), 'minute', hours * 60 + 1);
}
class SuspendUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
}
}
handleDurationChange = (event) => {
this.setState({duration: event.target.value});
}
handleDurationChange = event => {
this.setState({ duration: event.target.value });
};
handleMessageChange = (event) => {
this.setState({message: event.target.value});
}
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
goToStep1 = () => {
this.setState({
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
'suspenduser.email_message_suspend',
this.props.username,
this.props.organizationName,
timeago(durationsToDate(this.state.duration)),
timeago(durationsToDate(this.state.duration))
),
});
}
};
handlePerform = () => {
this.props.onPerform({
message: this.state.message,
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
};
renderStep0() {
const {onCancel, username} = this.props;
const {duration} = this.state;
const { onCancel, username } = this.props;
const { duration } = this.state;
return (
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
<legend className={styles.legend}>
{t('suspenduser.select_duration')}
</legend>
<RadioGroup
name='status filter'
name="status filter"
value={duration}
childContainer='div'
childContainer="div"
onChange={this.handleDurationChange}
className={styles.radioGroup}
>
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
</RadioGroup>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-confirm'
)}
onClick={this.goToStep1}
raised
>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {message} = this.state;
const {onCancel, username} = this.props;
const { message } = this.state;
const { onCancel, username } = this.props;
return (
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
<legend className={styles.legend}>
{t('suspenduser.write_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange} />
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button
cStyle="black"
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-send'
)}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
}
render() {
const {open, onCancel} = this.props;
const {step} = this.state;
const { open, onCancel } = this.props;
const { step } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
open={open}
>
<div className={styles.close}>
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
<button
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</button>
</div>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
import PropTypes from 'prop-types';
import cn from 'classnames';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
);
autosizingRenderInput.propTypes = {
onChange: PropTypes.func,
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
addTag: PropTypes.func,
};
const TagsInputComponent = ({className = '', ...props}) => {
const TagsInputComponent = ({ className = '', ...props }) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
pasteSplit={data => data.split(',').map(d => d.trim())}
className={cn(styles.root, 'tags-input', className)}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
@@ -207,7 +207,7 @@
.toastify__body {
color: white;
overflow-x: scroll;
overflow-x: auto;
font-size: 15px;
font-weight: 400;
}
@@ -1,6 +1,6 @@
import './ToastContainer.css';
import {defaultProps} from 'recompose';
import {ToastContainer} from 'react-toastify';
import { defaultProps } from 'recompose';
import { ToastContainer } from 'react-toastify';
export default defaultProps({
autoClose: 5000,
+141 -79
View File
@@ -2,78 +2,87 @@ import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import capitalize from 'lodash/capitalize';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import AccountHistory from './AccountHistory';
import {Slot} from 'coral-framework/components';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
import {
getReliability,
isSuspended,
isBanned,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {Icon, Drawer, Spinner, TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import {
Icon,
Drawer,
Spinner,
TabBar,
Tab,
TabContent,
TabPane,
} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
class UserDetail extends React.Component {
rejectThenReload = async (info) => {
rejectThenReload = async info => {
try {
await this.props.rejectComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
acceptThenReload = async (info) => {
acceptThenReload = async info => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
changeTab = (tab) => {
changeTab = tab => {
this.props.changeTab(tab);
}
};
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showSuspenUserDialog = () =>
this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () =>
this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -86,7 +95,7 @@ class UserDetail extends React.Component {
}
getActionMenuLabel() {
const {root: {user}} = this.props;
const { root: { user } } = this.props;
if (isBanned(user)) {
return 'Banned';
@@ -101,12 +110,7 @@ class UserDetail extends React.Component {
const {
data,
root,
root: {
me,
user,
totalComments,
rejectedComments,
},
root: { me, user, totalComments, rejectedComments },
activeTab,
selectedCommentIds,
toggleSelect,
@@ -120,7 +124,7 @@ class UserDetail extends React.Component {
} = this.props;
// if totalComments is 0, you're dividing by zero
let rejectedPercent = (rejectedComments / totalComments) * 100;
let rejectedPercent = rejectedComments / totalComments * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
@@ -131,43 +135,67 @@ class UserDetail extends React.Component {
return (
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer className="talk-admin-user-detail-drawer" onClose={hideUserDetail}>
<h3 className={cn(styles.username, 'talk-admin-user-detail-username')}>
<Drawer
className="talk-admin-user-detail-drawer"
onClose={hideUserDetail}
>
<h3
className={cn(styles.username, 'talk-admin-user-detail-username')}
>
{user.username}
</h3>
{user.id &&
{user.id && (
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-user-detail-actions-menu')}
buttonClassNames={cn({
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel()}>
{suspended ? <ActionsMenuItem
onClick={() => unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>}
{banned ? <ActionsMenuItem
onClick={() => unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>}
className={cn(
styles.actionsMenu,
'talk-admin-user-detail-actions-menu'
)}
buttonClassNames={cn(
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
}
)}
{(banned || suspended) && <UserInfoTooltip user={user} banned={banned} suspended={suspended} />}
{(banned || suspended) && (
<UserInfoTooltip
user={user}
banned={banned}
suspended={suspended}
/>
)}
<div>
<ul className={styles.userDetailList}>
@@ -177,13 +205,18 @@ class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({id}) =>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
icon="content_copy"
copyText={id}
/>
</li>
)}
))}
</ul>
<ul className={styles.stats}>
@@ -199,7 +232,12 @@ class UserDetail extends React.Component {
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span
className={cn(
styles.statReportResult,
styles[getReliability(user.reliable.flagger)]
)}
>
{capitalize(getReliability(user.reliable.flagger))}
</span>
</li>
@@ -209,7 +247,7 @@ class UserDetail extends React.Component {
<Slot
fill="userProfile"
data={this.props.data}
queryData={{root, user}}
queryData={{ root, user }}
/>
<hr />
@@ -218,28 +256,48 @@ class UserDetail extends React.Component {
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls='talk-admin-user-detail-content'
aria-controls="talk-admin-user-detail-content"
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}} >
}}
>
<Tab
tabId={'all'}
className={cn(styles.tab, styles.button, 'talk-admin-user-detail-all-tab')} >
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-all-tab'
)}
>
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')} >
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
</Tab>
<Tab tabId={'history'} className={cn(styles.tab, styles.button, 'talk-admin-user-detail-history-tab')}>
<Tab
tabId={'history'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-history-tab'
)}
>
Account History
</Tab>
</TabBar>
<TabContent activeTab={activeTab} className='talk-admin-user-detail-content'>
<TabPane tabId={'all'} className={'talk-admin-user-detail-all-tab-pane'}>
<TabContent
activeTab={activeTab}
className="talk-admin-user-detail-content"
>
<TabPane
tabId={'all'}
className={'talk-admin-user-detail-all-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -255,7 +313,10 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'rejected'} className={'talk-admin-user-detail-rejected-tab-pane'}>
<TabPane
tabId={'rejected'}
className={'talk-admin-user-detail-rejected-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -271,10 +332,11 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'history'} className={'talk-admin-user-detail-history-tab-pane'}>
<AccountHistory
userState={user.state}
/>
<TabPane
tabId={'history'}
className={'talk-admin-user-detail-history-tab-pane'}
>
<AccountHistory user={user} />
</TabPane>
</TabContent>
</Drawer>
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import { Link } from 'react-router';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({ commentId: this.props.comment.id });
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
render() {
const {
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
toggleSelect,
className,
data,
root: {settings: {wordlist: {banned, suspect}}},
root: { settings: { wordlist: { banned, suspect } } },
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
type="checkbox"
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
onChange={e => toggleSelect(e.target.value, e.target.checked)}
/>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.labels}>
<CommentLabels comment={comment} />
@@ -65,7 +69,11 @@ class UserDetailComment extends React.Component {
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
@@ -74,8 +82,7 @@ class UserDetailComment extends React.Component {
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>
{' '}
/>{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -106,11 +113,7 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
/>
<CommentDetails data={data} root={root} comment={comment} />
</li>
);
}
@@ -142,7 +145,7 @@ UserDetailComment.propTypes = {
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
};
@@ -7,17 +7,11 @@ import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = (props) => {
const UserDetailCommentList = props => {
const {
data,
root,
root: {
user,
comments: {
nodes,
hasNextPage
}
},
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
rejectComment,
selectedCommentIds,
@@ -30,39 +24,49 @@ const UserDetailCommentList = (props) => {
} = props;
return (
<div className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}>
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
<div
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
>
<div
className={
selectedCommentIds.length > 0
? cn(styles.bulkActionHeader, styles.selected)
: styles.bulkActionHeader
}
>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
<ApproveButton onClick={bulkAcceptThenReload} minimal />
<RejectButton onClick={bulkRejectThenReload} minimal />
<span className={styles.selectedCommentsInfo}>
{' '}
{selectedCommentIds.length} comments selected
</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
type="checkbox"
id="toogleAll"
checked={
selectedCommentIds.length > 0 &&
selectedCommentIds.length === nodes.length
}
onChange={e => {
toggleSelectAll(
nodes.map(comment => comment.id),
e.target.checked
);
}}
/>
<label htmlFor="toogleAll">Select all</label>
</div>
</div>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
{nodes.map(comment => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return (
<Comment
key={comment.id}
user={user}
root={root}
@@ -73,9 +77,9 @@ const UserDetailCommentList = (props) => {
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
/>
);
})}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
@@ -1,83 +1,165 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = {menuVisible: false};
const initialState = { menuVisible: false };
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({menuVisible: !this.state.menuVisible});
}
this.setState({ menuVisible: !this.state.menuVisible });
};
hideMenu = () => {
this.setState({menuVisible: false});
}
this.setState({ menuVisible: false });
};
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
}
};
render() {
const {menuVisible} = this.state;
const {user, banned, suspended} = this.props;
const { menuVisible } = this.state;
const { user, banned, suspended } = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span onClick={this.toogleMenu} className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}>
<span
onClick={this.toogleMenu}
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}>
{
banned && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-banned')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Banned On</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'banned').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'banned').assigned_by.username}</span>
</li>
</ul>
</div>
)
}
<div
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
>
{banned && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-banned'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Banned On</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'banned').assigned_by
.username
}
</span>
</li>
</ul>
</div>
)}
{
suspended && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-suspended')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Suspension</strong>
<span></span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'suspension').assigned_by.username}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Start</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>End</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').until)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
</ul>
</div>
)
}
{suspended && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-suspended'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Suspension</strong>
<span />
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'suspension')
.assigned_by.username
}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Start</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(
user,
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>End</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
</div>
)}
</div>
)}
</div>
+76 -62
View File
@@ -1,98 +1,112 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
import { Link, IndexLink } from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root
root,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</IndexLink>
)
}
<Link
id='storiesNav'
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
</IndexLink>
)}
<Link
id="storiesNav"
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
<Link
id="communityNav"
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
</Navigation>
) : null}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<IconButton
name="settings"
id="menu-settings"
className="talk-admin-header-settings-button"
/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem onClick={() => showShortcuts(true)}>
{t('configure.shortcuts')}
</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
<a
href="https://github.com/coralproject/talk/releases"
target="_blank"
rel="noopener noreferrer"
>
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
<a
href="https://support.coralproject.net"
target="_blank"
rel="noopener noreferrer"
>
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
<li>{`v${process.env.VERSION}`}</li>
</ul>
</div>
</div>
@@ -105,7 +119,7 @@ CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
root: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,7 +1,6 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
const Indicator = () => <span className={styles.indicator} />;
export default Indicator;
+4 -10
View File
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import styles from './Layout.css';
@@ -18,14 +18,8 @@ const Layout = ({
showShortcuts={toggleShortcutModal}
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
auth={auth}
/>
<div className={styles.layout}>
{children}
</div>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
@@ -34,7 +28,7 @@ Layout.propTypes = {
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
restricted: PropTypes.bool, // hide elements from a user that's logged out
};
export default Layout;
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
export const Logo = ({ className = '' }) => (
<div className={`${styles.logo} ${className}`}>
<h1>
<CoralLogo className={styles.base} />
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
);
Logo.propTypes = {
className: PropTypes.string
className: PropTypes.string,
};
+2 -1
View File
@@ -1 +1,2 @@
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS =
'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
+4 -2
View File
@@ -2,8 +2,10 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST =
'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS =
'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
+2 -1
View File
@@ -10,7 +10,8 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS =
'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
@@ -1,30 +1,39 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import BanUserDialog from '../components/BanUserDialog';
import {hideBanUserDialog} from '../actions/banUserDialog';
import {withBanUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {compose} from 'react-apollo';
import { hideBanUserDialog } from '../actions/banUserDialog';
import {
withBanUser,
withSetCommentStatus,
} from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {getErrorMessages} from 'coral-framework/utils';
import {notify} from 'coral-framework/actions/notification';
import { getErrorMessages } from 'coral-framework/utils';
import { notify } from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
const {userId, commentId, commentStatus, banUser, setCommentStatus, hideBanUserDialog, notify} = this.props;
const {
userId,
commentId,
commentStatus,
banUser,
setCommentStatus,
hideBanUserDialog,
notify,
} = this.props;
try {
await banUser({id: userId, message: ''});
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
}
};
getInfo() {
let note = t('bandialog.note_ban_user');
@@ -55,7 +64,9 @@ BanUserDialogContainer.propTypes = {
commentStatus: PropTypes.string,
};
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
banUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -63,18 +74,18 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideBanUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
notify,
},
dispatch
),
});
export default compose(
withBanUser,
withSetCommentStatus,
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps)
)(BanUserDialogContainer);
@@ -1,12 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
const slots = ['adminCommentDetailArea', 'adminCommentMoreDetails'];
export default withFragments({
root: gql`
@@ -20,5 +17,5 @@ export default withFragments({
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentDetails);
@@ -1,11 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentLabels from '../components/CommentLabels';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = [
'adminCommentLabels',
];
const slots = ['adminCommentLabels'];
export default withFragments({
root: gql`
@@ -30,5 +28,5 @@ export default withFragments({
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentLabels);
+21 -23
View File
@@ -1,28 +1,26 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
export default withQuery(gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: {
statuses: [PREMOD]
})
reportedCount: commentCount(query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
action_type: FLAG
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
export default withQuery(
gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: { statuses: [PREMOD] })
reportedCount: commentCount(
query: { statuses: [NONE, PREMOD, SYSTEM_WITHHELD], action_type: FLAG }
)
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
}
})
}
`, {
options: {
pollInterval: 10000
)
}
})(Header);
`,
{
options: {
pollInterval: 10000,
},
}
)(Header);
+35 -24
View File
@@ -1,35 +1,39 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import {FullLoading} from '../components/FullLoading';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import {
checkLogin,
handleLogin,
requestPasswordReset,
logout,
} from '../actions/auth';
import { can } from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends React.Component {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
const { checkLogin, fetchConfig } = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess
passwordRequestSuccess,
} = this.props.auth;
const {
@@ -61,7 +65,8 @@ class LayoutContainer extends React.Component {
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth} >
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
@@ -71,7 +76,10 @@ class LayoutContainer extends React.Component {
} else if (loggedIn) {
return (
<Layout {...this.props}>
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
@@ -89,22 +97,25 @@ LayoutContainer.propTypes = {
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func
fetchConfig: PropTypes.func,
};
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -1,33 +1,43 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
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 t, {timeago} from 'coral-framework/services/i18n';
import { hideSuspendUserDialog } from '../actions/suspendUserDialog';
import {
withSetCommentStatus,
withSuspendUser,
} from 'coral-framework/graphql/mutations';
import { compose, gql } from 'react-apollo';
import t, { timeago } from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import get from 'lodash/get';
import {notify} from 'coral-framework/actions/notification';
import { notify } from 'coral-framework/actions/notification';
class SuspendUserDialogContainer extends Component {
suspendUser = async ({message, until}) => {
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
suspendUser = async ({ message, until }) => {
const {
userId,
username,
commentStatus,
commentId,
hideSuspendUserDialog,
setCommentStatus,
suspendUser,
notify,
} = this.props;
hideSuspendUserDialog();
try {
await suspendUser({id: userId, message, until});
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until)),
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
};
@@ -53,14 +63,16 @@ SuspendUserDialogContainer.propTypes = {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
__typename
settings {
organizationName
}
}
`);
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
suspendUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -68,19 +80,19 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideSuspendUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideSuspendUserDialog,
notify,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps),
withSuspendUser,
withSetCommentStatus,
withOrganizationName,
withOrganizationName
)(SuspendUserDialogContainer);
+108 -78
View File
@@ -1,25 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { compose, gql } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
getDefinitionName,
getSlotFragmentSpreads,
} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
toggleSelectAllCommentInUserDetail
toggleSelectAllCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus, withUnbanUser, withUnsuspendUser} from 'coral-framework/graphql/mutations';
import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import { notify } from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
@@ -33,38 +40,36 @@ const commentConnectionFragment = gql`
${UserDetailComment.fragments.comment}
`;
const slots = [
'userProfile',
];
const slots = ['userProfile'];
class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = async (status) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
bulkSetCommentStatus = async status => {
const changes = this.props.selectedCommentIds.map(commentId => {
return this.props.setCommentStatus({ commentId, status });
});
await Promise.all(changes);
this.props.clearUserDetailSelections(); // un-select everything
}
};
bulkReject = () => {
return this.bulkSetCommentStatus('REJECTED');
}
};
bulkAccept = () => {
return this.bulkSetCommentStatus('ACCEPTED');
}
};
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
acceptComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'ACCEPTED' });
};
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
rejectComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'REJECTED' });
};
loadMore = () => {
if (this.isLoadingMore) {
@@ -78,24 +83,25 @@ class UserDetailContainer extends React.Component {
author_id: this.props.data.variables.author_id,
statuses: this.props.data.variables.statuses,
};
this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
return update(prev, {
comments: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
}
})
this.props.data
.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, { fetchMoreResult: { comments } }) => {
return update(prev, {
comments: {
nodes: { $push: comments.nodes },
hasNextPage: { $set: comments.hasNextPage },
startCursor: { $set: comments.startCursor },
endCursor: { $set: comments.endCursor },
},
});
},
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
.catch(err => {
this.isLoadingMore = false;
throw err;
});
@@ -107,24 +113,27 @@ class UserDetailContainer extends React.Component {
}
}
render () {
render() {
if (!this.props.userId) {
return null;
}
const loading = this.props.data.loading;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props} />;
return (
<UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props}
/>
);
}
}
@@ -142,15 +151,28 @@ UserDetailContainer.propTypes = {
};
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
query CoralAdmin_Moderation_LoadMore(
$limit: Int = 10
$cursor: Cursor
$author_id: ID!
$statuses: [COMMENT_STATUS!]
) {
comments(
query: {
limit: $limit
cursor: $cursor
author_id: $author_id
statuses: $statuses
}
) {
...CoralAdmin_UserDetail_CommentConnection
}
}
${commentConnectionFragment}
`;
export const withUserDetailQuery = withQuery(gql`
export const withUserDetailQuery = withQuery(
gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
@@ -171,6 +193,7 @@ export const withUserDetailQuery = withQuery(gql`
until
created_at
assigned_by {
id
username
}
}
@@ -180,6 +203,7 @@ export const withUserDetailQuery = withQuery(gql`
history {
status
assigned_by {
id
username
}
created_at
@@ -190,6 +214,7 @@ export const withUserDetailQuery = withQuery(gql`
history {
status
assigned_by {
id
username
}
created_at
@@ -215,36 +240,41 @@ export const withUserDetailQuery = withQuery(gql`
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
return {
variables: {author_id: userId, statuses},
fetchPolicy: 'network-only',
};
},
skip: (ownProps) => !ownProps.userId,
});
`,
{
options: ({ userId, statuses }) => {
return {
variables: { author_id: userId, statuses },
fetchPolicy: 'network-only',
};
},
skip: ownProps => !ownProps.userId,
}
);
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
userId: state.userDetail.userId,
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
modal: state.ui.modal
modal: state.ui.modal,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify
}, dispatch)
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify,
},
dispatch
),
});
export default compose(
@@ -252,5 +282,5 @@ export default compose(
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withUnsuspendUser
)(UserDetailContainer);
@@ -1,7 +1,7 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
@@ -35,10 +35,13 @@ export default withFragments({
editing {
edited
}
status_history {
type
}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
`,
})(UserDetailComment);
+136 -97
View File
@@ -1,6 +1,6 @@
import update from 'immutability-helper';
import {mapLeaves} from 'coral-framework/utils';
import {gql} from 'react-apollo';
import { mapLeaves } from 'coral-framework/utils';
import { gql } from 'react-apollo';
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
@@ -14,150 +14,189 @@ const userStatusFragment = gql`
}
}
}
}`;
}
`;
const userRoleFragment = gql`
fragment Talk_UpdateUserRole on User {
role
}`;
}
`;
export default {
mutations: {
SetUserRole: ({variables: {id, role}}) => ({
update: (proxy) => {
SetUserRole: ({ variables: { id, role } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userRoleFragment, id: fragmentId});
const data = proxy.readFragment({
fragment: userRoleFragment,
id: fragmentId,
});
const updated = update(data, {
role: {
$set: role
}
$set: role,
},
});
proxy.writeFragment({fragment: userRoleFragment, id: fragmentId, data: updated});
proxy.writeFragment({
fragment: userRoleFragment,
id: fragmentId,
data: updated,
});
},
}),
SuspendUser: ({variables: {input: {id, until}}}) => ({
update: (proxy) => {
SuspendUser: ({ variables: { input: { id, until } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
suspension: {
until: {$set: until}
}
}
}
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnsuspendUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
state: {
status: {
suspension: {
until: {$set: null}
}
}
}
until: { $set: until },
},
},
},
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
BanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
UnsuspendUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
banned: {
status: {$set: true}
}
}
}
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnbanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
state: {
status: {
banned: {
status: {$set: false}
}
}
}
suspension: {
until: { $set: null },
},
},
},
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
SetUserBanStatus: ({variables: {status, id}}) => ({
BanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: true },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
UnbanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: false },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
SetUserBanStatus: ({ variables: { status, id } }) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
TalkAdmin_Community: prev => {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
}),
ApproveUsername: ({variables: {id}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
},
});
return updated;
}
}
}),
RejectUsername: ({variables: {id: userId}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
},
},
}),
UpdateSettings: ({variables: {input}}) => ({
ApproveUsername: ({ variables: { id } }) => ({
updateQueries: {
TalkAdmin_Configure: (prev) => {
TalkAdmin_Community: prev => {
const updated = update(prev, {
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
flaggedUsers: {
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
},
},
}),
RejectUsername: ({ variables: { id: userId } }) => ({
updateQueries: {
TalkAdmin_Community: prev => {
const updated = update(prev, {
flaggedUsers: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== userId),
},
},
});
return updated;
},
},
}),
UpdateSettings: ({ variables: { input } }) => ({
updateQueries: {
TalkAdmin_Configure: prev => {
const updated = update(prev, {
settings: mapLeaves(input, leaf => ({ $set: leaf })),
});
return updated;
},
},
}),
},
};
+15 -9
View File
@@ -1,20 +1,20 @@
import React from 'react';
import {render} from 'react-dom';
import { render } from 'react-dom';
import smoothscroll from 'smoothscroll-polyfill';
import TalkProvider from 'coral-framework/components/TalkProvider';
import {createContext} from 'coral-framework/services/bootstrap';
import { createContext } from 'coral-framework/services/bootstrap';
import reducers from './reducers';
import App from './components/App';
import 'react-mdl/extra/material.js';
import graphqlExtension from './graphql';
import pluginsConfig from 'pluginsConfig';
import {toast} from 'react-toastify';
import {createNotificationService} from './services/notification';
import {hideShortcutsNote} from './actions/moderation';
import { toast } from 'react-toastify';
import { createNotificationService } from './services/notification';
import { hideShortcutsNote } from './actions/moderation';
smoothscroll.polyfill();
function init({store, storage}) {
function init({ store, storage }) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
@@ -22,13 +22,19 @@ function init({store, storage}) {
async function main() {
const notification = createNotificationService(toast);
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
const context = await createContext({
reducers,
graphqlExtension,
pluginsConfig,
notification,
init,
});
render(
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
</TalkProvider>,
document.querySelector('#root')
);
}
+53 -52
View File
@@ -5,60 +5,61 @@ const initialState = {
user: null,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null
passwordRequestSuccess: null,
};
export default function auth (state = initialState, action) {
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default :
return state;
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default:
return state;
}
}
@@ -1,4 +1,7 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
const initialState = {
open: false,
@@ -10,21 +13,21 @@ const initialState = {
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+67 -67
View File
@@ -8,7 +8,7 @@ import {
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
const initialState = {
@@ -23,75 +23,75 @@ const initialState = {
pagePeople: 0,
user: {},
banDialog: false,
rejectUsernameDialog: false
rejectUsernameDialog: false,
};
export default function community (state = initialState, action) {
export default function community(state = initialState, action) {
switch (action.type) {
case FETCH_USERS_REQUEST :
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE :
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS : {
case FETCH_USERS_REQUEST:
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE:
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS: {
const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SORT_UPDATE :
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default :
return state;
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SORT_UPDATE:
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true,
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default:
return state;
}
}
+9 -9
View File
@@ -1,17 +1,17 @@
import * as actions from '../actions/config';
const initialState = {
data: {}
data: {},
};
export default function config (state = initialState, action) {
export default function config(state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
}
}
+31 -28
View File
@@ -11,37 +11,40 @@ const initialState = {
export default function configure(state = initialState, action) {
switch (action.type) {
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
next = update(next, {
pending: action.updater,
});
}
if (action.errorUpdater) {
next = update(next, {
errors: action.errorUpdater,
});
}
const noErrors = Object.keys(next.errors).reduce(
(res, error) => res && !next.errors[error],
true
);
const canSave = !isEmpty(next.pending) && noErrors;
next = update(next, {
pending: action.updater,
canSave: { $set: canSave },
});
}
if (action.errorUpdater) {
next = update(next, {
errors: action.errorUpdater,
});
}
const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true);
const canSave = !isEmpty(next.pending) && noErrors;
next = update(next, {
canSave: {$set: canSave},
});
return next;
}
case actions.CLEAR_PENDING:
return {
...state,
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
return next;
}
case actions.CLEAR_PENDING:
return {
...state,
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
}
return state;
}
+99 -97
View File
@@ -8,127 +8,129 @@ const initialState = {
organizationName: '',
domains: {
whitelist: [],
}
},
},
user: {
username: '',
email: '',
password: '',
confirmPassword: ''
}
confirmPassword: '',
},
},
errors: {
organizationName: '',
username: '',
email: '',
password: '',
confirmPassword: ''
confirmPassword: '',
},
showErrors: false,
hasError: false,
error: null,
step: 0,
navItems: [{
text: '1. Add Organization Name',
step: 1
},
{
text: '2. Create your account',
step: 2
},
{
text: '3. Domain Whitelist',
step: 3
}],
navItems: [
{
text: '1. Add Organization Name',
step: 1,
},
{
text: '2. Create your account',
step: 2,
},
{
text: '3. Domain Whitelist',
step: 3,
},
],
installRequest: null,
installRequestError: null,
alreadyInstalled: false
alreadyInstalled: false,
};
export default function install (state = initialState, action) {
export default function install(state = initialState, action) {
switch (action.type) {
case actions.NEXT_STEP:
return {
...state,
step: state.step + 1,
};
case actions.PREVIOUS_STEP:
return {
...state,
step: state.step - 1,
};
case actions.GO_TO_STEP:
return {
...state,
step: action.step,
};
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
return update(state, {
data: {
settings: {
domains: {
whitelist: {$set: action.value},
case actions.NEXT_STEP:
return {
...state,
step: state.step + 1,
};
case actions.PREVIOUS_STEP:
return {
...state,
step: state.step - 1,
};
case actions.GO_TO_STEP:
return {
...state,
step: action.step,
};
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
return update(state, {
data: {
settings: {
domains: {
whitelist: { $set: action.value },
},
},
},
},
});
case actions.UPDATE_FORMDATA_SETTINGS:
return update(state, {
data: {
settings: {
[action.name]: {$set: action.value},
});
case actions.UPDATE_FORMDATA_SETTINGS:
return update(state, {
data: {
settings: {
[action.name]: { $set: action.value },
},
},
},
});
case actions.UPDATE_FORMDATA_USER:
return update(state, {
data: {
user: {
[action.name]: {$set: action.value},
});
case actions.UPDATE_FORMDATA_USER:
return update(state, {
data: {
user: {
[action.name]: { $set: action.value },
},
},
},
});
case actions.HAS_ERROR:
return {
...state,
hasError: true,
showErrors: true,
};
case actions.ADD_ERROR:
return update(state, {
errors: {
[action.name]: {$set: action.error},
},
});
case actions.CLEAR_ERRORS:
return {
...state,
errors: {},
};
case actions.INSTALL_REQUEST:
return {
...state,
isLoading: true,
};
case actions.INSTALL_SUCCESS:
return {
...state,
isLoading: false,
installRequest: 'SUCCESS',
};
case actions.INSTALL_FAILURE:
return {
...state,
isLoading: false,
installRequest: 'FAILURE',
installRequestError: action.error
};
case actions.CHECK_INSTALL_SUCCESS:
return {
...state,
alreadyInstalled: action.installed,
};
default :
return state;
});
case actions.HAS_ERROR:
return {
...state,
hasError: true,
showErrors: true,
};
case actions.ADD_ERROR:
return update(state, {
errors: {
[action.name]: { $set: action.error },
},
});
case actions.CLEAR_ERRORS:
return {
...state,
errors: {},
};
case actions.INSTALL_REQUEST:
return {
...state,
isLoading: true,
};
case actions.INSTALL_SUCCESS:
return {
...state,
isLoading: false,
installRequest: 'SUCCESS',
};
case actions.INSTALL_FAILURE:
return {
...state,
isLoading: false,
installRequest: 'FAILURE',
installRequestError: action.error,
};
case actions.CHECK_INSTALL_SUCCESS:
return {
...state,
alreadyInstalled: action.installed,
};
default:
return state;
}
}
+48 -48
View File
@@ -10,54 +10,54 @@ const initialState = {
selectedCommentId: '',
};
export default function moderation (state = initialState, action) {
export default function moderation(state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
};
case actions.TOGGLE_MODAL:
return {
...state,
modalOpen: action.open,
};
case actions.SINGLE_VIEW:
return {
...state,
singleView: !state.singleView,
};
case actions.HIDE_SHORTCUTS_NOTE:
return {
...state,
shortcutsNoteVisible: 'hide',
};
case actions.SHOW_STORY_SEARCH:
return {
...state,
storySearchVisible: true,
};
case actions.HIDE_STORY_SEARCH:
return {
...state,
storySearchVisible: false,
};
case actions.STORY_SEARCH_CHANGE_VALUE:
return {
...state,
storySearchString: action.value,
};
case actions.SET_SORT_ORDER:
return {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
default:
return state;
case actions.MODERATION_CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
};
case actions.TOGGLE_MODAL:
return {
...state,
modalOpen: action.open,
};
case actions.SINGLE_VIEW:
return {
...state,
singleView: !state.singleView,
};
case actions.HIDE_SHORTCUTS_NOTE:
return {
...state,
shortcutsNoteVisible: 'hide',
};
case actions.SHOW_STORY_SEARCH:
return {
...state,
storySearchVisible: true,
};
case actions.HIDE_STORY_SEARCH:
return {
...state,
storySearchVisible: false,
};
case actions.STORY_SEARCH_CHANGE_VALUE:
return {
...state,
storySearchString: action.value,
};
case actions.SET_SORT_ORDER:
return {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
default:
return state;
}
}
+51 -52
View File
@@ -5,7 +5,7 @@ const initialState = {
assets: {
byId: {},
ids: [],
assets: []
assets: [],
},
searchValue: '',
criteria: {
@@ -14,60 +14,59 @@ const initialState = {
},
};
export default function assets (state = initialState, action) {
export default function assets(state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
return update(state, {
assets: {
totalPages: {$set: action.totalPages},
page: {$set: action.page},
byId: {$set: assets},
count: {$set: action.count},
ids: {$set: Object.keys(assets)},
},
});
}
case actions.UPDATE_ASSET_STATE_REQUEST:
return update(state, {
assets: {
byId: {
[action.id]: {
closedAt: {$set: action.closedAt},
return update(state, {
assets: {
totalPages: { $set: action.totalPages },
page: { $set: action.page },
byId: { $set: assets },
count: { $set: action.count },
ids: { $set: Object.keys(assets) },
},
});
}
case actions.UPDATE_ASSET_STATE_REQUEST:
return update(state, {
assets: {
byId: {
[action.id]: {
closedAt: { $set: action.closedAt },
},
},
},
},
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: {$set: action.assets},
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
case actions.SET_CRITERIA:
return {
...state,
criteria: {
...state.criteria,
...action.criteria,
},
};
default:
return state;
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: { $set: action.assets },
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
case actions.SET_CRITERIA:
return {
...state,
criteria: {
...state.criteria,
...action.criteria,
},
};
default:
return state;
}
}
@@ -1,4 +1,7 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
const initialState = {
open: false,
@@ -10,21 +13,21 @@ const initialState = {
export default function suspendUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+32 -26
View File
@@ -1,33 +1,39 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
const initialState = {
modal: false
modal: false,
};
export default function config (state = initialState, action) {
export default function config(state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
modal: true,
};
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
modal: false,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
case SHOW_BAN_USER_DIALOG:
return {
...state,
modal: true,
};
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
modal: false,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
}
}
+41 -39
View File
@@ -9,44 +9,46 @@ const initialState = {
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case actions.VIEW_USER_DETAIL:
return {
...state,
userId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userId: null,
selectedCommentIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_TAB_USER_DETAIL:
return {
...state,
activeTab: action.tab,
statuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: [...state.selectedCommentIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: state.selectedCommentIds.filter((id) => id !== action.id),
};
case actions.SELECT_ALL_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: action.ids
};
default:
return state;
case actions.VIEW_USER_DETAIL:
return {
...state,
userId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userId: null,
selectedCommentIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_TAB_USER_DETAIL:
return {
...state,
activeTab: action.tab,
statuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: [...state.selectedCommentIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: state.selectedCommentIds.filter(
id => id !== action.id
),
};
case actions.SELECT_ALL_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: action.ids,
};
default:
return state;
}
}
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './Community.css';
import People from '../containers/People';
@@ -8,23 +8,22 @@ import FlaggedAccounts from '../containers/FlaggedAccounts';
class Community extends Component {
renderTab() {
const {route, community, ...props} = this.props;
const { route, community, ...props } = this.props;
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return <People
community={community}
data={this.props.data}
root={this.props.root}
/>;
return (
<People
community={community}
data={this.props.data}
root={this.props.root}
/>
);
}
return (
<div>
<FlaggedAccounts
data={this.props.data}
root={this.props.root}
/>
<FlaggedAccounts data={this.props.data} root={this.props.root} />
<RejectUsernameDialog
user={community.user}
open={community.rejectUsernameDialog}
@@ -36,14 +35,12 @@ class Community extends Component {
}
render() {
const {root: {flaggedUsernamesCount}} = this.props;
const { root: { flaggedUsernamesCount } } = this.props;
return (
<div className="talk-admin-community">
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
<div className={styles.container}>
{this.renderTab()}
</div>
<div className={styles.container}>{this.renderTab()}</div>
</div>
);
}
@@ -1,23 +1,33 @@
import React from 'react';
import styles from './CommunityMenu.css';
import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import CountBadge from '../../../components/CountBadge';
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
const CommunityMenu = ({ flaggedUsernamesCount = 0 }) => {
const flaggedPath = '/admin/community/flagged';
const peoplePath = '/admin/community/people';
return (
<div className='mdl-tabs'>
<div className="mdl-tabs">
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
<Link
to={flaggedPath}
className={`mdl-tabs__tab ${
styles.tab
} talk-admin-nav-flagged-accounts`}
activeClassName={styles.active}
>
{t('community.flaggedaccounts')}
<CountBadge count={flaggedUsernamesCount} />
</Link>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
<Link
to={peoplePath}
className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`}
activeClassName={styles.active}
>
{t('community.people')}
</Link>
</div>
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
import FlaggedUser from '../containers/FlaggedUser';
import {CSSTransitionGroup} from 'react-transition-group';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './FlaggedAccounts.css';
class FlaggedAccounts extends React.Component {
@@ -22,45 +22,45 @@ class FlaggedAccounts extends React.Component {
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
<div
className={cn(
'talk-adnin-community-flagged-accounts',
styles.container
)}
>
<div className={styles.mainFlaggedContent}>
{
hasResults
? <CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
users.nodes.map((user) => {
return (
<FlaggedUser
user={user}
key={user.id}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})
}
</CSSTransitionGroup>
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
{hasResults ? (
<CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{users.nodes.map(user => {
return (
<FlaggedUser
user={user}
key={user.id}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})}
</CSSTransitionGroup>
) : (
<EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
)}
<LoadMore loadMore={loadMore} showLoadMore={users.hasNextPage} />
</div>
</div>
);
@@ -3,7 +3,7 @@ import styles from './FlaggedUser.css';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import { username } from 'talk-plugin-flags/helpers/flagReasons';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
@@ -16,79 +16,94 @@ const shortReasons = {
};
class User extends React.Component {
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
showRejectUsernameDialog = () =>
this.props.showRejectUsernameDialog({ id: this.props.user.id });
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
approveUser = () =>
this.props.approveUser({
userId: this.props.user.id,
});
render() {
const {
user,
viewUserDetail,
selected,
className,
} = this.props;
const { user, viewUserDetail, selected, className } = this.props;
return (
<li tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
<li
tabIndex={0}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div
className={cn('talk-admin-community-flagged-user', styles.container)}
>
<div
className={cn(
'talk-admin-community-flagged-user-header',
styles.header
)}
>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
className={styles.button}>
<button onClick={this.viewAuthorDetail} className={styles.button}>
{user.username}
</button>
</div>
</div>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
<div
className={cn(
'talk-admin-community-flagged-user-body',
styles.body
)}
>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
<span className={styles.flaggedByLabel}>
{t('community.flags')}({ user.actions.length })
{t('community.flags')}({user.actions.length})
</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{user.action_summaries.map((action, i) => {
return (
<span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</span>
);
})}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
{user.action_summaries.map((action_sum, i) => {
return (
<div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
return (
<p className={styles.flaggedByReason} key={j}>
{action.user && (
<button
onClick={() => {
viewUserDetail(action.user.id);
}}
className={styles.button}
>
{action.user.username}
</button>
)}
: {action.message ? action.message : 'n/a'}
</p>
);
}
return null;
}
)}
</div>;
}
)}
</div>
);
})}
</div>
</div>
<div className={styles.sideActions}>
@@ -1,38 +1,37 @@
import React from 'react';
import cn from 'classnames';
import styles from './People.css';
import {Icon, Dropdown, Option} from 'coral-ui';
import { Icon, Dropdown, Option } from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
import t from 'coral-framework/services/i18n';
import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import {isSuspended, isBanned} from 'coral-framework/utils/user';
import { isSuspended, isBanned } from 'coral-framework/utils/user';
import moment from 'moment';
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
field: 'username',
},
{
title: t('community.account_creation_date'),
field: 'created_at'
field: 'created_at',
},
{
title: t('community.status'),
field: 'status'
field: 'status',
},
{
title: t('community.newsroom_role'),
field: 'role'
}
field: 'role',
},
];
class People extends React.Component {
getActionMenuLabel = (user) => {
getActionMenuLabel = user => {
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
@@ -41,23 +40,23 @@ class People extends React.Component {
return '';
};
unsuspendUser = (input) => {
unsuspendUser = input => {
this.props.unsuspendUser(input);
}
};
unbanUser = (input) => {
unbanUser = input => {
this.props.unbanUser(input);
}
};
showBanUserDialog = (input) => {
showBanUserDialog = input => {
this.props.showBanUserDialog(input);
}
};
showSuspendUserDialog = (input) => {
showSuspendUserDialog = input => {
this.props.showSuspendUserDialog(input);
}
};
render () {
render() {
const {
onSearchChange,
users = [],
@@ -69,107 +68,169 @@ class People extends React.Component {
const hasResults = !!users.nodes.length;
return (
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div
className={cn(
styles.container,
'talk-admin-community-people-container'
)}
>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<Icon name="search" className={styles.searchIcon} />
<input
id="commenters-search"
type="text"
className={styles.searchBoxInput}
defaultValue=''
defaultValue=""
onChange={onSearchChange}
placeholder={t('streams.search')}
/>
</div>
</div>
<div className={styles.mainContent}>
{
hasResults
? <div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col" >
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.nodes.map((user)=> (
<tr key={user.id} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(user.id);}} className={cn(styles.username, styles.button)}>{user.username}</button>
<span className={styles.email}>{user.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format('MMMM Do YYYY, h:mm:ss a')}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-community-people-dd-status')}
buttonClassNames={cn(styles.actionsMenuButton, {
[styles.actionsMenuSuspended]: isSuspended(user),
[styles.actionsMenuBanned]: isBanned(user),
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel(user)} >
{isSuspended(user) ? <ActionsMenuItem
onClick={() => this.unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showSuspendUserDialog({
userId: user.id,
username: user.username,
})}>
Suspend User
</ActionsMenuItem>}
{isBanned(user) ? <ActionsMenuItem
onClick={() => this.unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showBanUserDialog({
userId: user.id,
username: user.username,
})}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={(role) => setUserRole(user.id, role)}>
<Option value={'COMMENTER'} label={t('community.commenter')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
{hasResults ? (
<div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) => (
<th
key={i}
className={cn(
'mdl-data-table__cell--non-numeric',
styles.header
)}
scope="col"
>
{header.title}
</th>
))}
</tbody>
</table>
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</tr>
</thead>
<tbody>
{users.nodes.map(user => (
<tr
key={user.id}
className="talk-admin-community-people-row"
>
<td className="mdl-data-table__cell--non-numeric">
<button
onClick={() => {
viewUserDetail(user.id);
}}
className={cn(styles.username, styles.button)}
>
{user.username}
</button>
<span className={styles.email}>
{user.profiles.map(({ id }) => id)}
</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format(
'MMMM Do YYYY, h:mm:ss a'
)}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(
styles.actionsMenu,
'talk-admin-community-people-dd-status'
)}
buttonClassNames={cn(
styles.actionsMenuButton,
{
[styles.actionsMenuSuspended]: isSuspended(
user
),
[styles.actionsMenuBanned]: isBanned(user),
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel(user)}
>
{isSuspended(user) ? (
<ActionsMenuItem
onClick={() =>
this.unsuspendUser({ id: user.id })
}
>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showSuspendUserDialog({
userId: user.id,
username: user.username,
})
}
>
Suspend User
</ActionsMenuItem>
)}
{isBanned(user) ? (
<ActionsMenuItem
onClick={() => this.unbanUser({ id: user.id })}
>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showBanUserDialog({
userId: user.id,
username: user.username,
})
}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={role => setUserRole(user.id, role)}
>
<Option
value={'COMMENTER'}
label={t('community.commenter')}
/>
<Option
value={'STAFF'}
label={t('community.staff')}
/>
<Option
value={'MODERATOR'}
label={t('community.moderator')}
/>
<Option
value={'ADMIN'}
label={t('community.admin')}
/>
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
</div>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
) : (
<EmptyCard>{t('community.no_results')}</EmptyCard>
)}
</div>
</div>
);
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Dialog, Button} from 'coral-ui';
import { Dialog, Button } from 'coral-ui';
import styles from './RejectUsernameDialog.css';
import t from 'coral-framework/services/i18n';
@@ -11,26 +11,28 @@ const stages = [
title: 'reject_username.title_reject',
description: 'reject_username.description_reject',
options: {
'j': 'reject_username.no_cancel',
'k': 'reject_username.yes_suspend'
}
j: 'reject_username.no_cancel',
k: 'reject_username.yes_suspend',
},
},
{
title: 'reject_username.title_notify',
description: 'reject_username.description_notify',
options: {
'j': 'bandialog.cancel',
'k': 'reject_username.send'
}
}
j: 'bandialog.cancel',
k: 'reject_username.send',
},
},
];
class RejectUsernameDialog extends Component {
state = {email: '', stage: 0}
class RejectUsernameDialog extends Component {
state = { email: '', stage: 0 };
componentDidMount() {
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
this.setState({
email: t('reject_username.email_message_reject'),
about: t('reject_username.username'),
});
}
/*
@@ -38,53 +40,59 @@ class RejectUsernameDialog extends Component {
* handles the possible actions for that dialog.
*/
onActionClick = (stage, menuOption) => () => {
const {rejectUsername, user} = this.props;
const {stage} = this.state;
const { rejectUsername, user } = this.props;
const { stage } = this.state;
const cancel = this.props.handleClose;
const next = () => this.setState({stage: stage + 1});
const next = () => this.setState({ stage: stage + 1 });
const suspend = async () => {
try {
await rejectUsername(user.id);
this.props.handleClose();
} catch (err) {
// TODO: handle error.
console.error(err);
}
};
const suspendModalActions = [
[ cancel, next ],
[ cancel, suspend ]
];
const suspendModalActions = [[cancel, next], [cancel, suspend]];
return suspendModalActions[stage][menuOption]();
}
};
onEmailChange = (e) => {
this.setState({email: e.target.value});
}
onEmailChange = e => {
this.setState({ email: e.target.value });
};
render () {
const {open, handleClose} = this.props;
const {stage} = this.state;
render() {
const { open, handleClose } = this.props;
const { stage } = this.state;
return <Dialog
className={cn(styles.suspendDialog, 'talk-admin-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={cn(styles.container, `talk-admin-reject-username-dialog-step-${stage}`)}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
return (
<Dialog
className={cn(
styles.suspendDialog,
'talk-admin-reject-username-dialog'
)}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}
>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div
className={cn(
styles.container,
`talk-admin-reject-username-dialog-step-${stage}`
)}
>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{/* {
{/* {
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
stage === 1 &&
@@ -99,18 +107,28 @@ class RejectUsernameDialog extends Component {
</div>
</div>
} */}
<div className={cn(styles.modalButtons, 'talk-admin-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button
key={i}
className={cn('talk-admin-username-dialog-button', `talk-admin-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
<div
className={cn(
styles.modalButtons,
'talk-admin-reject-username-dialog-buttons'
)}
>
{Object.keys(stages[stage].options).map((key, i) => (
<Button
key={i}
className={cn(
'talk-admin-username-dialog-button',
`talk-admin-reject-username-dialog-button-${key}`
)}
onClick={this.onActionClick(stage, i)}
>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</div>
</Dialog>;
</Dialog>
);
}
}
@@ -1,25 +1,29 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName} from 'coral-framework/utils';
import {withRejectUsername} from 'coral-framework/graphql/mutations';
import { getDefinitionName } from 'coral-framework/utils';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import People from '../containers/People';
import {hideRejectUsernameDialog} from '../../../actions/community';
import { hideRejectUsernameDialog } from '../../../actions/community';
import Community from '../components/Community';
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
community: state.community,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
hideRejectUsernameDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
hideRejectUsernameDialog,
},
dispatch
);
const withData = withQuery(gql`
const withData = withQuery(
gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(
query:{
@@ -43,11 +47,13 @@ const withData = withQuery(gql`
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -1,29 +1,28 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {Spinner} from 'coral-ui';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { Spinner } from 'coral-ui';
import PropTypes from 'prop-types';
import {withApproveUsername} from 'coral-framework/graphql/mutations';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import { withApproveUsername } from 'coral-framework/graphql/mutations';
import { showRejectUsernameDialog } from '../../../actions/community';
import { viewUserDetail } from '../../../actions/userDetail';
import { getDefinitionName } from 'coral-framework/utils';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
}
approveUser = ({userId: id}) => {
approveUser = ({ userId: id }) => {
return this.props.approveUsername(id);
}
};
loadMore = () => {
return this.props.data.fetchMore({
@@ -32,14 +31,14 @@ class FlaggedAccountsContainer extends Component {
limit: 5,
cursor: this.props.root.flaggedUsers.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{flaggedUsers}}) => {
updateQuery: (previous, { fetchMoreResult: { flaggedUsers } }) => {
const updated = update(previous, {
flaggedUsers: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, flaggedUsers.nodes),
$apply: nodes => appendNewNodes(nodes, flaggedUsers.nodes),
},
hasNextPage: {$set: flaggedUsers.hasNextPage},
endCursor: {$set: flaggedUsers.endCursor},
hasNextPage: { $set: flaggedUsers.hasNextPage },
endCursor: { $set: flaggedUsers.endCursor },
},
});
return updated;
@@ -53,7 +52,11 @@ class FlaggedAccountsContainer extends Component {
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
return (
<div>
<Spinner />
</div>
);
}
return (
@@ -102,11 +105,14 @@ const LOAD_MORE_QUERY = gql`
${FlaggedUser.fragments.user}
`;
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showRejectUsernameDialog,
viewUserDetail,
},
dispatch
);
export default compose(
connect(null, mapDispatchToProps),
@@ -136,5 +142,5 @@ export default compose(
}
${FlaggedUser.fragments.user}
`,
}),
})
)(FlaggedAccountsContainer);
@@ -1,18 +1,18 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import FlaggedUser from '../components/FlaggedUser';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
__typename
}
`,
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
id
}
`,
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
@@ -31,7 +31,7 @@ export default withFragments({
}
}
role
actions{
actions {
id
created_at
... on FlagAction {
@@ -50,5 +50,5 @@ export default withFragments({
}
}
}
`
`,
})(FlaggedUser);
@@ -1,50 +1,54 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import People from '../components/People';
import PropTypes from 'prop-types';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {withUnbanUser, withUnsuspendUser, withSetUserRole} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import { withFragments } from 'plugin-api/beta/client/hocs';
import {
withUnbanUser,
withUnsuspendUser,
withSetUserRole,
} from 'coral-framework/graphql/mutations';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
import { Spinner } from 'coral-ui';
class PeopleContainer extends React.Component {
timer = null;
state = {
searchValue: ''
searchValue: '',
};
onSearchChange = (e) => {
const {value} = e.target;
this.setState({searchValue: value}, () => {
onSearchChange = e => {
const { value } = e.target;
this.setState({ searchValue: value }, () => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.search(value);
}, 350);
});
}
};
search = async (value) => {
search = async value => {
return this.props.data.fetchMore({
query: SEARCH_QUERY,
variables: {
value,
limit: 5,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
users: {
nodes: {
$set: users.nodes,
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
hasNextPage: { $set: users.hasNextPage },
endCursor: { $set: users.endCursor },
},
});
return updated;
@@ -54,7 +58,7 @@ class PeopleContainer extends React.Component {
setUserRole = async (id, role) => {
await this.props.setUserRole(id, role);
}
};
loadMore = () => {
return this.props.data.fetchMore({
@@ -64,14 +68,14 @@ class PeopleContainer extends React.Component {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
$apply: nodes => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
hasNextPage: { $set: users.hasNextPage },
endCursor: { $set: users.endCursor },
},
});
return updated;
@@ -80,28 +84,33 @@ class PeopleContainer extends React.Component {
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
return (
<div>
<Spinner />
</div>
);
}
return <People
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>;
return (
<People
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>
);
}
}
@@ -117,20 +126,23 @@ PeopleContainer.propTypes = {
root: PropTypes.object,
};
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
},
dispatch
);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers($limit: Int, $cursor: Cursor, $value: String) {
users(query: {
value: $value,
limit: $limit,
cursor: $cursor
}){
query TalkAdminCommunity_People_LoadMoreUsers(
$limit: Int
$cursor: Cursor
$value: String
) {
users(query: { value: $value, limit: $limit, cursor: $cursor }) {
hasNextPage
endCursor
nodes {
@@ -160,10 +172,7 @@ const LOAD_MORE_QUERY = gql`
const SEARCH_QUERY = gql`
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
users(query: {
value: $value,
limit: $limit,
}){
users(query: { value: $value, limit: $limit }) {
hasNextPage
endCursor
nodes {
@@ -199,7 +208,7 @@ export default compose(
withFragments({
root: gql`
fragment TalkAdminCommunity_People_root on RootQuery {
users(query: {}){
users(query: {}) {
hasNextPage
endCursor
nodes {
@@ -226,5 +235,5 @@ export default compose(
}
}
`,
}),
})
)(PeopleContainer);
@@ -20,7 +20,7 @@
.mainContent {
width: calc(100% - 300px);
padding: 10px 14px 80px 14px;
padding: 10px 14px 120px 14px;
box-sizing: border-box;
max-width: 718px;
}
@@ -1,74 +1,77 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import {Button, List, Item} from 'coral-ui';
import { Button, List, Item } from 'coral-ui';
import styles from './Configure.css';
import StreamSettings from '../containers/StreamSettings';
import ModerationSettings from '../containers/ModerationSettings';
import TechSettings from '../containers/TechSettings';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import PropTypes from 'prop-types';
export default class Configure extends Component {
getSectionComponent(section) {
switch(section){
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
switch (section) {
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
}
throw new Error(`Unknown section ${section}`);
}
render () {
const {auth: {user}, canSave, savePending, setActiveSection, activeSection} = this.props;
render() {
const {
auth: { user },
canSave,
savePending,
setActiveSection,
activeSection,
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
if (!can(user, 'UPDATE_CONFIG')) {
return <p>You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!</p>;
return (
<p>
You must be an administrator to access config settings. Please find
the nearest Admin and ask them to level you up!
</p>
);
}
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={setActiveSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
<Item itemId="stream" icon="speaker_notes">
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
<Item itemId="moderation" icon="thumbs_up_down">
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
<Item itemId="tech" icon="code">
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
{
canSave ?
<Button
raised
onClick={savePending}
className={styles.changedSave}
icon='check'
full
>
{t('configure.save_changes')}
</Button>
:
<Button
raised
disabled
icon='check'
full
>
{t('configure.save_changes')}
</Button>
}
{canSave ? (
<Button
raised
onClick={savePending}
className={styles.changedSave}
icon="check"
full
>
{t('configure.save_changes')}
</Button>
) : (
<Button raised disabled icon="check" full>
{t('configure.save_changes')}
</Button>
)}
</div>
</div>
<div className={styles.mainContent}>
<SectionComponent
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import styles from './ConfigurePage.css';
const ConfigurePage = ({title, children, ...rest}) => (
const ConfigurePage = ({ title, children, ...rest }) => (
<div {...rest}>
<h3 className={styles.title}>{title}</h3>
{children}
@@ -4,14 +4,14 @@ import TagsInput from 'coral-admin/src/components/TagsInput';
import t from 'coral-framework/services/i18n';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
const Domainlist = ({domains, onChangeDomainlist}) => {
const Domainlist = ({ domains, onChangeDomainlist }) => {
return (
<ConfigureCard title={t('configure.domain_list_title')}>
<p>{t('configure.domain_list_text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
inputProps={{ placeholder: 'URL' }}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</ConfigureCard>
);
@@ -1,15 +1,14 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import t from 'coral-framework/services/i18n';
import join from 'url-join';
import styles from './EmbedLink.css';
import {Button} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
import { Button } from 'coral-ui';
import { BASE_URL } from 'coral-framework/constants/url';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
import {stripIndent} from 'common-tags';
import { stripIndent } from 'common-tags';
class EmbedLink extends Component {
state = {copied: false};
state = { copied: false };
copyToClipBoard = () => {
const copyTextarea = document.querySelector(`.${styles.embedInput}`);
@@ -17,13 +16,13 @@ class EmbedLink extends Component {
try {
document.execCommand('copy');
this.setState({copied: true});
this.setState({ copied: true });
} catch (err) {
console.error('Unable to copy', err);
}
}
};
render () {
render() {
const coralJsUrl = join(BASE_URL, '/static/embed.js');
const embedText = stripIndent`
<div id="coral_talk_stream"></div>
@@ -37,9 +36,20 @@ class EmbedLink extends Component {
return (
<ConfigureCard title={'Embed Comment Stream'}>
<p>{t('configure.copy_and_paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<textarea
rows={5}
type="text"
className={styles.embedInput}
value={embedText}
readOnly={true}
/>
<div className={styles.actions}>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
<Button
raised
className={styles.copyButton}
onClick={this.copyToClipBoard}
cStyle="black"
>
{t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>
@@ -7,44 +7,55 @@ import ConfigurePage from './ConfigurePage';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
class ModerationSettings extends React.Component {
updateModeration = () => {
const updater = {moderation: {$set: this.props.settings.moderation === 'PRE' ? 'POST' : 'PRE'}};
this.props.updatePending({updater});
const updater = {
moderation: {
$set: this.props.settings.moderation === 'PRE' ? 'POST' : 'PRE',
},
};
this.props.updatePending({ updater });
};
updateEmailConfirmation = () => {
const updater = {requireEmailConfirmation: {$set: !this.props.settings.requireEmailConfirmation}};
this.props.updatePending({updater});
const updater = {
requireEmailConfirmation: {
$set: !this.props.settings.requireEmailConfirmation,
},
};
this.props.updatePending({ updater });
};
updatePremodLinksEnable = () => {
const updater = {premodLinksEnable: {$set: !this.props.settings.premodLinksEnable}};
this.props.updatePending({updater});
const updater = {
premodLinksEnable: { $set: !this.props.settings.premodLinksEnable },
};
this.props.updatePending({ updater });
};
updateWordlist = (listName, list) => {
this.props.updatePending({updater: {
wordlist: {$apply: (wordlist) => {
const changeSet = {[listName]: list};
if (!wordlist) {
return changeSet;
}
return {
...wordlist,
...changeSet,
};
}},
}});
this.props.updatePending({
updater: {
wordlist: {
$apply: wordlist => {
const changeSet = { [listName]: list };
if (!wordlist) {
return changeSet;
}
return {
...wordlist,
...changeSet,
};
},
},
},
});
};
render() {
const {settings, data, root, updatePending, errors} = this.props;
const { settings, data, root, updatePending, errors } = this.props;
return (
<ConfigurePage
title={t('configure.moderation_settings')}
>
<ConfigurePage title={t('configure.moderation_settings')}>
<ConfigureCard
checked={settings.requireEmailConfirmation}
onCheckbox={this.updateEmailConfirmation}
@@ -69,11 +80,12 @@ class ModerationSettings extends React.Component {
<Wordlist
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
onChangeWordlist={this.updateWordlist} />
onChangeWordlist={this.updateWordlist}
/>
<Slot
fill="adminModerationSettings"
data={data}
queryData={{root, settings}}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
@@ -1,9 +1,9 @@
import React from 'react';
import {SelectField, Option} from 'react-mdl-selectfield';
import { SelectField, Option } from 'react-mdl-selectfield';
import t from 'coral-framework/services/i18n';
import styles from './StreamSettings.css';
import {Textfield} from 'react-mdl';
import {Icon, TextArea} from 'coral-ui';
import { Textfield } from 'react-mdl';
import { Icon, TextArea } from 'coral-ui';
import PropTypes from 'prop-types';
import Slot from 'coral-framework/components/Slot';
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
@@ -14,12 +14,12 @@ import ConfigureCard from 'coral-framework/components/ConfigureCard';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
days: 60 * 60 * 24,
hours: 60 * 60
hours: 60 * 60,
};
// To see if we are talking about weeks, days or hours
// We talk the remainder of the division and see if it's 0
const getTimeoutMeasure = (ts) => {
const getTimeoutMeasure = ts => {
if (ts % TIMESTAMPS['weeks'] === 0) {
return 'weeks';
} else if (ts % TIMESTAMPS['days'] === 0) {
@@ -31,113 +31,125 @@ const getTimeoutMeasure = (ts) => {
// Dividing the amount by it's measure (hours, days, weeks) we
// obtain the amount of time
const getTimeoutAmount = (ts) => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
class StreamSettings extends React.Component {
updateCharCountEnable = () => {
const updater = {charCountEnable: {$set: !this.props.settings.charCountEnable}};
this.props.updatePending({updater});
const updater = {
charCountEnable: { $set: !this.props.settings.charCountEnable },
};
this.props.updatePending({ updater });
};
updateCharCount = (event) => {
updateCharCount = event => {
let error = null;
const charCount = event.target.value;
if (charCount.match(/[^0-9]/) || charCount.length === 0) {
error = true;
}
const updater = {charCount: {$set: charCount}};
const errorUpdater = {charCount: {$set: error}};
const updater = { charCount: { $set: charCount } };
const errorUpdater = { charCount: { $set: error } };
this.props.updatePending({updater, errorUpdater});
this.props.updatePending({ updater, errorUpdater });
};
updateInfoBoxEnable = () => {
const updater = {infoBoxEnable: {$set: !this.props.settings.infoBoxEnable}};
this.props.updatePending({updater});
const updater = {
infoBoxEnable: { $set: !this.props.settings.infoBoxEnable },
};
this.props.updatePending({ updater });
};
updateInfoBoxContent = (value) => {
const updater = {infoBoxContent: {$set: value}};
this.props.updatePending({updater});
updateInfoBoxContent = value => {
const updater = { infoBoxContent: { $set: value } };
this.props.updatePending({ updater });
};
updateClosedMessage = (event) => {
const updater = {closedMessage: {$set: event.target.value}};
this.props.updatePending({updater});
updateClosedMessage = event => {
const updater = { closedMessage: { $set: event.target.value } };
this.props.updatePending({ updater });
};
updateEditCommentWindowLength = (e) => {
updateEditCommentWindowLength = e => {
const value = e.target.value;
const valueAsNumber = parseFloat(value);
const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
const milliseconds = !isNaN(valueAsNumber) && valueAsNumber * 1000;
const updater = {editCommentWindowLength: {$set: milliseconds || value}};
this.props.updatePending({updater});
const updater = {
editCommentWindowLength: { $set: milliseconds || value },
};
this.props.updatePending({ updater });
};
updateAutoClose = () => {
const updater = {autoCloseStream: {$set: !this.props.settings.autoCloseStream}};
this.props.updatePending({updater});
const updater = {
autoCloseStream: { $set: !this.props.settings.autoCloseStream },
};
this.props.updatePending({ updater });
};
updateClosedTimeout = (event) => {
updateClosedTimeout = event => {
const val = event.target.value;
const measure = getTimeoutMeasure(this.props.settings.closedTimeout);
const updater = {closedTimeout: {$set: val * TIMESTAMPS[measure]}};
this.props.updatePending({updater});
const updater = { closedTimeout: { $set: val * TIMESTAMPS[measure] } };
this.props.updatePending({ updater });
};
// If we are changing the measure we need to recalculate using the old amount
// Same thing if we are just changing the amount
updateClosedTimeoutMeasure = (event) => {
updateClosedTimeoutMeasure = event => {
const amount = getTimeoutAmount(this.props.settings.closedTimeout);
const updater = {closedTimeout: {$set: amount * TIMESTAMPS[event]}};
this.props.updatePending({updater});
const updater = { closedTimeout: { $set: amount * TIMESTAMPS[event] } };
this.props.updatePending({ updater });
};
render() {
const {settings, data, root, errors, updatePending} = this.props;
const { settings, data, root, errors, updatePending } = this.props;
return (
<ConfigurePage
title={t('configure.stream_settings')}
>
<ConfigurePage title={t('configure.stream_settings')}>
<ConfigureCard
checked={settings.charCountEnable}
onCheckbox={this.updateCharCountEnable}
title={t('configure.comment_count_header')}
>
<span>{t('configure.comment_count_text_pre')}</span>
<input type='text'
className={cn(styles.inlineTextfield, styles.charCountTexfield, settings.charCountEnable && styles.charCountTexfieldEnable)}
htmlFor='charCount'
<input
type="text"
className={cn(
styles.inlineTextfield,
styles.charCountTexfield,
settings.charCountEnable && styles.charCountTexfieldEnable
)}
htmlFor="charCount"
onChange={this.updateCharCount}
value={settings.charCount}
disabled={settings.charCountEnable ? '' : 'disabled'}
/>
<span>{t('configure.comment_count_text_post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{t('configure.comment_count_error')}
</span>
}
{errors.charCount && (
<span className={styles.settingsError}>
<br />
<Icon name="error_outline" />
{t('configure.comment_count_error')}
</span>
)}
</ConfigureCard>
<ConfigureCard
checked={settings.infoBoxEnable}
onCheckbox={this.updateInfoBoxEnable}
title={t('configure.include_comment_stream')}
>
<p>
{t('configure.include_comment_stream_desc')}
</p>
<div className={cn(styles.configSettingInfoBox, settings.infoBoxEnable ? null : styles.hidden)} >
<p>{t('configure.include_comment_stream_desc')}</p>
<div
className={cn(
styles.configSettingInfoBox,
settings.infoBoxEnable ? null : styles.hidden
)}
>
<MarkdownEditor
className={styles.descriptionBox}
onChange={this.updateInfoBoxContent}
@@ -152,25 +164,30 @@ class StreamSettings extends React.Component {
>
<p>{t('configure.closed_comments_desc')}</p>
<div>
<TextArea className={styles.descriptionBox}
<TextArea
className={styles.descriptionBox}
onChange={this.updateClosedMessage}
value={settings.closedMessage}
/>
</div>
</ConfigureCard>
<ConfigureCard
title={t('configure.edit_comment_timeframe_heading')}
>
<ConfigureCard title={t('configure.edit_comment_timeframe_heading')}>
{t('configure.edit_comment_timeframe_text_pre')}
&nbsp;
<input
className={cn(styles.inlineTextfield, styles.editCommentTimeframeTextfield)}
className={cn(
styles.inlineTextfield,
styles.editCommentTimeframeTextfield
)}
type="number"
min="0"
onChange={this.updateEditCommentWindowLength}
placeholder="30"
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
pattern='[0-9]+([\.][0-9]*)?'
defaultValue={
settings.editCommentWindowLength /
1000 /* saved as ms, rendered as seconds */
}
pattern="[0-9]+([\.][0-9]*)?"
/>
&nbsp;
{t('configure.edit_comment_timeframe_text_post')}
@@ -182,17 +199,19 @@ class StreamSettings extends React.Component {
>
<div className={styles.autoCloseWrapper}>
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
type="number"
pattern="[0-9]+"
style={{ width: 50 }}
onChange={this.updateClosedTimeout}
value={getTimeoutAmount(settings.closedTimeout)}
label={t('configure.closed_comments_label')} />
label={t('configure.closed_comments_label')}
/>
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={this.updateClosedTimeoutMeasure}>
onChange={this.updateClosedTimeoutMeasure}
>
<Option value={'hours'}>{t('configure.hours')}</Option>
<Option value={'days'}>{t('configure.days')}</Option>
<Option value={'weeks'}>{t('configure.weeks')}</Option>
@@ -204,7 +223,7 @@ class StreamSettings extends React.Component {
<Slot
fill="adminStreamSettings"
data={data}
queryData={{root, settings}}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
@@ -222,4 +241,3 @@ StreamSettings.propTypes = {
};
export default StreamSettings;
@@ -9,48 +9,51 @@ import ConfigurePage from './ConfigurePage';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
class TechSettings extends React.Component {
updateCustomCssUrl = (event) => {
const updater = {customCssUrl: {$set: event.target.value}};
this.props.updatePending({updater});
updateCustomCssUrl = event => {
const updater = { customCssUrl: { $set: event.target.value } };
this.props.updatePending({ updater });
};
updateDomainlist = (listName, list) => {
this.props.updatePending({updater: {
domains: {$apply: (domains) => {
const changeSet = {[listName]: list};
if (!domains) {
return changeSet;
}
return {
...domains,
...changeSet,
};
}},
}});
this.props.updatePending({
updater: {
domains: {
$apply: domains => {
const changeSet = { [listName]: list };
if (!domains) {
return changeSet;
}
return {
...domains,
...changeSet,
};
},
},
},
});
};
render() {
const {settings, data, root, errors, updatePending} = this.props;
const { settings, data, root, errors, updatePending } = this.props;
return (
<ConfigurePage
title={t('configure.tech_settings')}
>
<ConfigurePage title={t('configure.tech_settings')}>
<Domainlist
domains={settings.domains.whitelist}
onChangeDomainlist={this.updateDomainlist} />
onChangeDomainlist={this.updateDomainlist}
/>
<EmbedLink />
<ConfigureCard title={t('configure.custom_css_url')}>
<p>{t('configure.custom_css_url_desc')}</p>
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={this.updateCustomCssUrl} />
onChange={this.updateCustomCssUrl}
/>
</ConfigureCard>
<Slot
fill="adminTechSettings"
data={data}
queryData={{root, settings}}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
@@ -4,22 +4,23 @@ import TagsInput from 'coral-admin/src/components/TagsInput';
import PropTypes from 'prop-types';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
<div>
<ConfigureCard title={t('configure.banned_words_title')}>
<p>{t('configure.banned_word_text')}</p>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
onChange={(tags) => onChangeWordlist('banned', tags)}
inputProps={{ placeholder: 'word or phrase' }}
onChange={tags => onChangeWordlist('banned', tags)}
/>
</ConfigureCard>
<ConfigureCard title={t('configure.suspect_word_title')}>
<p>{t('configure.suspect_word_text')}</p>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
onChange={(tags) => onChangeWordlist('suspect', tags)} />
inputProps={{ placeholder: 'word or phrase' }}
onChange={tags => onChangeWordlist('suspect', tags)}
/>
</ConfigureCard>
</div>
);
@@ -1,51 +1,52 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import {withQuery, withMergedSettings} from 'coral-framework/hocs';
import {Spinner} from 'coral-ui';
import {notify} from 'coral-framework/actions/notification';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withQuery, withMergedSettings } from 'coral-framework/hocs';
import { Spinner } from 'coral-ui';
import { notify } from 'coral-framework/actions/notification';
import PropTypes from 'prop-types';
import {withUpdateSettings} from 'coral-framework/graphql/mutations';
import {getErrorMessages, getDefinitionName} from 'coral-framework/utils';
import { withUpdateSettings } from 'coral-framework/graphql/mutations';
import { getErrorMessages, getDefinitionName } from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
import {clearPending, setActiveSection} from '../../../actions/configure';
import { clearPending, setActiveSection } from '../../../actions/configure';
import Configure from '../components/Configure';
class ConfigureContainer extends Component {
savePending = async () => {
try {
await this.props.updateSettings(this.props.pending);
this.props.clearPending();
}
catch(err) {
} catch (err) {
this.props.notify('error', getErrorMessages(err));
}
};
render () {
if(this.props.data.loading) {
return <Spinner/>;
render() {
if (this.props.data.loading) {
return <Spinner />;
}
return <Configure
notify={this.props.notify}
auth={this.props.auth}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
activeSection={this.props.activeSection}
/>;
return (
<Configure
notify={this.props.notify}
auth={this.props.auth}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
activeSection={this.props.activeSection}
/>
);
}
}
const withConfigureQuery = withQuery(gql`
const withConfigureQuery = withQuery(
gql`
query TalkAdmin_Configure {
settings {
...${getDefinitionName(StreamSettings.fragments.settings)}
@@ -62,31 +63,36 @@ const withConfigureQuery = withQuery(gql`
${TechSettings.fragments.settings}
${ModerationSettings.fragments.root}
${ModerationSettings.fragments.settings}
`, {
options: () => ({
variables: {},
}),
});
`,
{
options: () => ({
variables: {},
}),
}
);
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
auth: state.auth,
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify,
clearPending,
setActiveSection,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
clearPending,
setActiveSection,
},
dispatch
);
export default compose(
withUpdateSettings,
withConfigureQuery,
connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('root.settings', 'pending', 'mergedSettings'),
withMergedSettings('root.settings', 'pending', 'mergedSettings')
)(ConfigureContainer);
ConfigureContainer.propTypes = {
@@ -1,23 +1,24 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import ModerationSettings from '../components/ModerationSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import {updatePending} from '../../../actions/configure';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
const slots = [
'adminModerationSettings',
];
const slots = ['adminModerationSettings'];
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
updatePending,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
updatePending,
},
dispatch
);
export default compose(
withFragments({
@@ -38,7 +39,7 @@ export default compose(
}
${getSlotFragmentSpreads(slots, 'settings')}
}
`
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps)
)(ModerationSettings);
@@ -1,23 +1,24 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import StreamSettings from '../components/StreamSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import {updatePending} from '../../../actions/configure';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
const slots = [
'adminStreamSettings',
];
const slots = ['adminStreamSettings'];
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
updatePending,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
updatePending,
},
dispatch
);
export default compose(
withFragments({
@@ -39,7 +40,7 @@ export default compose(
closedMessage
${getSlotFragmentSpreads(slots, 'settings')}
}
`
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps)
)(StreamSettings);
@@ -1,23 +1,24 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import TechSettings from '../components/TechSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import {updatePending} from '../../../actions/configure';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
const slots = [
'adminTechSettings',
];
const slots = ['adminTechSettings'];
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
errors: state.configure.errors,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
updatePending,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
updatePending,
},
dispatch
);
export default compose(
withFragments({
@@ -35,7 +36,7 @@ export default compose(
}
${getSlotFragmentSpreads(slots, 'settings')}
}
`
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps)
)(TechSettings);
@@ -1 +1 @@
export {default} from './containers/Configure';
export { default } from './containers/Configure';
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import styles from './Install.css';
import {Wizard, WizardNav} from 'coral-ui';
import { Wizard, WizardNav } from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
import InitialStep from './Steps/InitialStep';
@@ -10,68 +10,72 @@ import PermittedDomainsStep from './Steps/PermittedDomainsStep';
import FinalStep from './Steps/FinalStep';
export default class Install extends Component {
handleDomainsChange = (value) => {
handleDomainsChange = value => {
this.props.updatePermittedDomains(value);
};
handleSettingsChange = ({currentTarget: {name, value}}) => {
handleSettingsChange = ({ currentTarget: { name, value } }) => {
this.props.updateSettingsFormData(name, value);
};
handleUserChange = ({currentTarget: {name, value}}) => {
handleUserChange = ({ currentTarget: { name, value } }) => {
this.props.updateUserFormData(name, value);
};
handleSettingsSubmit = (e) => {
handleSettingsSubmit = e => {
e.preventDefault();
this.props.submitSettings();
};
handleUserSubmit = (e) => {
handleUserSubmit = e => {
e.preventDefault();
this.props.submitUser();
}
};
render() {
const {install} = this.props;
const { install } = this.props;
return (
<Layout restricted={true}>
<div className={styles.install}>
{
!install.alreadyInstalled ? (
<div>
<h2 className={styles.header}>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard
{!install.alreadyInstalled ? (
<div>
<h2 className={styles.header}>Welcome to the Coral Project</h2>
{install.step !== 0 ? (
<WizardNav
items={install.navItems}
currentStep={install.step}
nextStep={this.props.nextStep}
previousStep={this.props.previousStep}
goToStep={this.props.goToStep}
>
<InitialStep/>
<AddOrganizationName
install={install}
handleSettingsChange={this.handleSettingsChange}
handleSettingsSubmit={this.handleSettingsSubmit}
/>
<CreateYourAccount
install={install}
handleUserChange={this.handleUserChange}
handleUserSubmit={this.handleUserSubmit}
/>
<PermittedDomainsStep
install={install}
handleDomainsChange={this.handleDomainsChange}
finishInstall={this.props.finishInstall}
/>
<FinalStep/>
</Wizard>
</div>
) : (
<div>Talk is already installed</div>
)
}
icon="check"
/>
) : null}
<Wizard
currentStep={install.step}
nextStep={this.props.nextStep}
previousStep={this.props.previousStep}
goToStep={this.props.goToStep}
>
<InitialStep />
<AddOrganizationName
install={install}
handleSettingsChange={this.handleSettingsChange}
handleSettingsSubmit={this.handleSettingsSubmit}
/>
<CreateYourAccount
install={install}
handleUserChange={this.handleUserChange}
handleUserSubmit={this.handleUserSubmit}
/>
<PermittedDomainsStep
install={install}
handleDomainsChange={this.handleDomainsChange}
finishInstall={this.props.finishInstall}
/>
<FinalStep />
</Wizard>
</div>
) : (
<div>Talk is already installed</div>
)}
</div>
</Layout>
);
@@ -1,12 +1,12 @@
import React from 'react';
import styles from './style.css';
import {TextField, Button} from 'coral-ui';
import { TextField, Button } from 'coral-ui';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
const AddOrganizationName = (props) => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
const AddOrganizationName = props => {
const { handleSettingsChange, handleSettingsSubmit, install } = props;
return (
<div className={cn(styles.step, 'talk-install-step-2')}>
<p>{t('install.add_organization.description')}</p>
@@ -19,8 +19,16 @@ const AddOrganizationName = (props) => {
label={t('install.add_organization.label')}
onChange={handleSettingsChange}
showErrors={install.showErrors}
errorMsg={install.errors.organizationName} />
<Button className="talk-install-step-2-save-button" type="submit" cStyle='black' full>{t('install.add_organization.save')}</Button>
errorMsg={install.errors.organizationName}
/>
<Button
className="talk-install-step-2-save-button"
type="submit"
cStyle="black"
full
>
{t('install.add_organization.save')}
</Button>
</form>
</div>
</div>
@@ -1,12 +1,12 @@
import React from 'react';
import styles from './style.css';
import {TextField, Button, Spinner} from 'coral-ui';
import { TextField, Button, Spinner } from 'coral-ui';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
const InitialStep = (props) => {
const {handleUserChange, handleUserSubmit, install} = props;
const InitialStep = props => {
const { handleUserChange, handleUserSubmit, install } = props;
return (
<div className={cn(styles.step, 'talk-install-step-3')}>
<div className={styles.form}>
@@ -52,13 +52,23 @@ const InitialStep = (props) => {
errorMsg={install.errors.confirmPassword}
/>
{
!props.install.isLoading ?
<Button className="talk-install-step-3-save-button" cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
}
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
{!props.install.isLoading ? (
<Button
className="talk-install-step-3-save-button"
cStyle="black"
type="submit"
full
>
{t('install.create.save')}
</Button>
) : (
<Spinner />
)}
{props.install.installRequest === 'FAILURE' && (
<div className={styles.error}>
Error: {props.install.installRequestError}
</div>
)}
</form>
</div>
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
import {Link} from 'react-router';
import { Button } from 'coral-ui';
import { Link } from 'react-router';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
@@ -10,8 +10,12 @@ const InitialStep = () => {
return (
<div className={cn(styles.step, styles.finalStep, 'talk-install-step-5')}>
<p>{t('install.final.description')}</p>
<Button raised><Link to='/admin'>{t('install.final.launch')}</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">{t('install.final.close')}</a></Button>
<Button raised>
<Link to="/admin">{t('install.final.launch')}</Link>
</Button>
<Button cStyle="black" raised>
<a href="http://coralproject.net">{t('install.final.close')}</a>
</Button>
</div>
);
};
@@ -1,16 +1,23 @@
import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
const InitialStep = (props) => {
const {nextStep} = props;
const InitialStep = props => {
const { nextStep } = props;
return (
<div className={cn(styles.step, 'talk-install-step-1')}>
<p>{t('install.initial.description')}</p>
<Button className={'talk-install-get-started-button'} cStyle='green' onClick={nextStep} raised>{t('install.initial.submit')}</Button>
<Button
className={'talk-install-get-started-button'}
cStyle="green"
onClick={nextStep}
raised
>
{t('install.initial.submit')}
</Button>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More