Merge branch 'master' of github.com:coralproject/talk into 155836892

* 'master' of github.com:coralproject/talk:
  PropTypes
  Removin actions, using routes, and continuing the transition when the user makes an action
  Adding size, and proptypes
  IfSlotIsNotEmpty Bug
  Style and translations
  SaveChangesDialog
  Spanish translations
  Adding translations
  alert route hook ready!
  Restore Links
  Save changes / discard changes dialohg
  router and state lifted
  routes
This commit is contained in:
okbel
2018-04-06 17:59:25 -03:00
15 changed files with 282 additions and 66 deletions
+14 -2
View File
@@ -2,10 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
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';
import Configure from 'routes/Configure';
import StreamSettings from './routes/Configure/containers/StreamSettings';
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
import TechSettings from './routes/Configure/containers/TechSettings';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
@@ -15,7 +20,14 @@ const routes = (
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="configure" component={Configure}>
<Route path="stream" component={StreamSettings} />
<Route path="moderation" component={ModerationSettings} />
<Route path="tech" component={TechSettings} />
<IndexRedirect to="stream" />
</Route>
<Route path="stories" component={Stories} />
{/* Community Routes */}
+6 -2
View File
@@ -8,6 +8,10 @@ export const clearPending = () => {
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
export const showSaveDialog = () => {
return { type: actions.SHOW_SAVE_DIALOG };
};
export const hideSaveDialog = () => {
return { type: actions.HIDE_SAVE_DIALOG };
};
@@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE';
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`;
export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`;
+15 -6
View File
@@ -6,11 +6,23 @@ const initialState = {
canSave: false,
pending: {},
errors: {},
activeSection: 'stream',
saveDialog: false,
};
export default function configure(state = initialState, action) {
switch (action.type) {
case actions.SHOW_SAVE_DIALOG: {
return {
...state,
saveDialog: true,
};
}
case actions.HIDE_SAVE_DIALOG: {
return {
...state,
saveDialog: false,
};
}
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
@@ -40,11 +52,8 @@ export default function configure(state = initialState, action) {
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
default:
return state;
}
return state;
}
@@ -1,50 +1,37 @@
import React, { Component } from 'react';
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 React from 'react';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import { Button, List, Item } from 'coral-ui';
import { can } from 'coral-framework/services/perms';
import styles from './Configure.css';
import SaveChangesDialog from './SaveChangesDialog';
export default class Configure extends Component {
getSectionComponent(section) {
switch (section) {
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
}
throw new Error(`Unknown section ${section}`);
}
class Configure extends React.Component {
render() {
const {
currentUser,
canSave,
savePending,
setActiveSection,
activeSection,
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
const { canSave, currentUser, root, savePending, settings } = this.props;
if (!can(currentUser, '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>{t('configure.access_message')}</p>;
}
const passProps = {
root,
settings,
};
return (
<div className={styles.container}>
<SaveChangesDialog
saveDialog={this.props.saveDialog}
hideSaveDialog={this.props.hideSaveDialog}
saveChanges={this.props.saveChanges}
discardChanges={this.props.discardChanges}
/>
<div className={styles.leftColumn}>
<List onChange={setActiveSection} activeItem={activeSection}>
<List
onChange={this.props.handleSectionChange}
activeItem={this.props.activeSection}
>
<Item itemId="stream" icon="speaker_notes">
{t('configure.stream_settings')}
</Item>
@@ -74,10 +61,7 @@ export default class Configure extends Component {
</div>
</div>
<div className={styles.mainContent}>
<SectionComponent
root={this.props.root}
settings={this.props.settings}
/>
{React.cloneElement(this.props.children, passProps)}
</div>
</div>
);
@@ -86,10 +70,17 @@ export default class Configure extends Component {
Configure.propTypes = {
savePending: PropTypes.func.isRequired,
saveChanges: PropTypes.func.isRequired,
discardChanges: PropTypes.func.isRequired,
currentUser: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
setActiveSection: PropTypes.func.isRequired,
handleSectionChange: PropTypes.func.isRequired,
activeSection: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
saveDialog: PropTypes.bool,
hideSaveDialog: PropTypes.func.isRequired,
};
export default Configure;
@@ -0,0 +1,40 @@
.buttonActions {
padding-top: 15px;
text-align: right;
}
.dialog {
padding: 25px;
min-width: 400px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.title {
font-size: 18px;
font-weight: 800;
margin-bottom: 20px;
}
.cancel {
color: #363636;
margin-right: 15px;
display: inline-block;
&:hover {
cursor: pointer;
}
}
.button {
margin-left: 5px;
}
@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Button, Dialog } from 'coral-ui';
import styles from './SaveChangesDialog.css';
import t from 'coral-framework/services/i18n';
const SaveChangesDialog = ({
saveDialog,
hideSaveDialog,
saveChanges,
discardChanges,
}) => (
<Dialog
className={cn(styles.dialog, 'talk-admin-configure-save-dialog')}
id="saveDialog"
open={saveDialog}
onCancel={hideSaveDialog}
>
<span className={styles.close} onClick={hideSaveDialog}>
×
</span>
<div className={styles.title}>
{t('configure.save_changes_dialog.unsaved_changes')}
</div>
{t('configure.save_changes_dialog.copy')}
<div
className={cn(
styles.buttonActions,
'talk-admin-configure-save-dialog-button-actions'
)}
>
<a className={styles.cancel} onClick={hideSaveDialog}>
Cancel
</a>
<Button onClick={discardChanges} className={styles.button}>
{t('configure.save_changes_dialog.discard')}
</Button>
<Button onClick={saveChanges} cStyle="green" className={styles.button}>
{t('configure.save_changes_dialog.save_settings')}
</Button>
</div>
</Dialog>
);
SaveChangesDialog.propTypes = {
saveDialog: PropTypes.bool.isRequired,
hideSaveDialog: PropTypes.func.isRequired,
saveChanges: PropTypes.func.isRequired,
discardChanges: PropTypes.func.isRequired,
};
export default SaveChangesDialog;
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
@@ -10,15 +10,70 @@ import { 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,
showSaveDialog,
hideSaveDialog,
} from '../../../actions/configure';
import Configure from '../components/Configure';
import { withRouter } from 'react-router';
class ConfigureContainer extends React.Component {
state = { nextRoute: '' };
class ConfigureContainer extends Component {
savePending = async () => {
await this.props.updateSettings(this.props.pending);
this.props.clearPending();
};
saveChanges = async () => {
await this.savePending();
this.props.hideSaveDialog();
this.gotoNextRoute();
};
discardChanges = async () => {
await this.props.clearPending();
this.props.hideSaveDialog();
this.gotoNextRoute();
};
gotoNextRoute = () => {
const { nextRoute } = this.state;
if (nextRoute) {
this.props.router.push(nextRoute);
this.setState({ nextRoute: '' });
}
};
handleSectionChange = async section => {
const nextRoute = `/admin/configure/${section}`;
if (this.shouldShowSaveDialog()) {
await this.setState({ nextRoute });
this.props.showSaveDialog();
} else {
// Just go to the section
this.props.router.push(nextRoute);
}
};
shouldShowSaveDialog = () => {
return !!Object.keys(this.props.pending).length;
};
routeLeave = ({ pathname }) => {
if (this.shouldShowSaveDialog()) {
this.setState({ nextRoute: pathname });
this.props.showSaveDialog();
return false;
}
};
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
@@ -30,14 +85,20 @@ class ConfigureContainer extends Component {
return (
<Configure
saveChanges={this.saveChanges}
discardChanges={this.discardChanges}
saveDialog={this.props.saveDialog}
activeSection={this.props.routes[3].path}
hideSaveDialog={this.props.hideSaveDialog}
canSave={this.props.canSave}
currentUser={this.props.currentUser}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
handleSectionChange={this.handleSectionChange}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
activeSection={this.props.activeSection}
/>
>
{this.props.children}
</Configure>
);
}
}
@@ -74,18 +135,21 @@ const mapStateToProps = state => ({
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
saveDialog: state.configure.saveDialog,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
clearPending,
setActiveSection,
showSaveDialog,
hideSaveDialog,
},
dispatch
);
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withUpdateSettings,
withConfigureQuery,
@@ -93,14 +157,20 @@ export default compose(
)(ConfigureContainer);
ConfigureContainer.propTypes = {
activeSection: PropTypes.string,
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
showSaveDialog: PropTypes.func.isRequired,
hideSaveDialog: PropTypes.func.isRequired,
saveDialog: PropTypes.bool.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
pending: PropTypes.object.isRequired,
mergedSettings: PropTypes.object.isRequired,
activeSection: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
router: PropTypes.object,
route: PropTypes.object,
routes: PropTypes.array,
};
@@ -34,6 +34,7 @@ class Comment extends React.Component {
defaultComponent={CommentContent}
className={cn(styles.commentBody, 'my-comment-body')}
passthrough={slotPassthrough}
size={1}
/>
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span
@@ -7,7 +7,7 @@ class IfSlotIsNotEmpty extends React.Component {
isSlotEmpty(props = this.props) {
const { slotElements } = props;
return slotElements.length === 0
? false
? true
: slotElements.every(elements => elements.length === 0);
}
@@ -19,6 +19,7 @@ class IfSlotIsNotEmpty extends React.Component {
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
slotElements: PropTypes.array.isRequired,
children: PropTypes.node.isRequired,
passthrough: PropTypes.object.isRequired,
};
+7
View File
@@ -154,12 +154,19 @@ en:
sign_out: "Sign Out"
stories: Stories
stream_settings: "Stream Settings"
access_message: "You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!"
suspect_word_title: "Suspect words list"
suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
tech_settings: "Tech Settings"
title: "Configure Comment Stream"
weeks: Weeks
wordlist: "Banned Words"
save_changes_dialog:
unsaved_changes: "Unsaved changes"
copy: "You have made one or more changes without saving. Would you like to save or discard your changes?"
save_settings: "Save Settings"
discard: "Discard"
cancel: "Cancel"
continue: "Continue"
createdisplay:
check_the_form: "Invalid Form. Please check the fields"
+7
View File
@@ -153,12 +153,19 @@ es:
sign_out: "Desconectar"
stories: Artículos
stream_settings: "Configuración de Comentarios"
access_message: "Usted debe ser un administrador para acceder a esta página. Encuentre a otro admin y actualice los permisos de su cuenta!"
suspect_word_title: "Lista de palabras sospechosas"
suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando mayusculas y minúsculas, serán automáticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pegar una lista de palabras separadas por coma."
tech_settings: "Configuración Técnica"
title: "Configurar los comentarios"
weeks: Semanas
wordlist: "Palabras Suspendidas"
save_changes_dialog:
unsaved_changes: "Cambios no guardados"
copy: Has hecho uno o más cambios sin guardar. Deseas guardar o descartar tus cambios?"
save_settings: "Guardar configuración"
discard: "Descartar"
cancel: "Cancelar"
continue: "Continuar"
createdisplay:
check_the_form: "Formulario Inválido. Por favor verifica los campos"
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './Comment.css';
import { t } from 'plugin-api/beta/client/services';
@@ -28,6 +29,7 @@ class Comment extends React.Component {
fill="commentContent"
defaultComponent={CommentContent}
passthrough={slotPassthrough}
size={1}
/>
<div className={cn(`${pluginName}-comment-username-box`)}>
@@ -87,4 +89,11 @@ class Comment extends React.Component {
}
}
Comment.propTypes = {
viewComment: PropTypes.func,
comment: PropTypes.object,
asset: PropTypes.object,
root: PropTypes.object,
};
export default Comment;
@@ -1,12 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import Linkify from 'react-linkify';
import styles from './AdminCommentContent.css';
import { AdminCommentContent as Content } from 'plugin-api/beta/client/components';
class AdminCommentContent extends React.Component {
render() {
const { comment, suspectWords, bannedWords } = this.props;
return (
const content = (
<Content
className={styles.content}
body={comment.richTextBody ? comment.richTextBody : comment.body}
@@ -15,6 +16,12 @@ class AdminCommentContent extends React.Component {
html={!!comment.richTextBody}
/>
);
if (!!comment.richTextBody) {
return content;
}
return <Linkify properties={{ target: '_blank' }}>{content}</Linkify>;
}
}
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { PLUGIN_NAME } from '../constants';
import cn from 'classnames';
import styles from './CommentContent.css';
import Linkify from 'react-linkify';
class CommentContent extends React.Component {
render() {
@@ -14,7 +15,9 @@ class CommentContent extends React.Component {
dangerouslySetInnerHTML={{ __html: comment.richTextBody }}
/>
) : (
<div className={className}>{comment.body}</div>
<Linkify properties={{ target: '_blank' }}>
<div className={className}>{comment.body}</div>
</Linkify>
);
}
}