diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9d68d592b..28255aeb5 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -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 = ( - + + + + + + + + {/* Community Routes */} diff --git a/client/coral-admin/src/actions/configure.js b/client/coral-admin/src/actions/configure.js index acc30be1b..47c7fd25f 100644 --- a/client/coral-admin/src/actions/configure.js +++ b/client/coral-admin/src/actions/configure.js @@ -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 }; }; diff --git a/client/coral-admin/src/constants/configure.js b/client/coral-admin/src/constants/configure.js index 05673b5aa..9ab22580d 100644 --- a/client/coral-admin/src/constants/configure.js +++ b/client/coral-admin/src/constants/configure.js @@ -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`; diff --git a/client/coral-admin/src/reducers/configure.js b/client/coral-admin/src/reducers/configure.js index 9809b0fa9..c87463423 100644 --- a/client/coral-admin/src/reducers/configure.js +++ b/client/coral-admin/src/reducers/configure.js @@ -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; } diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index efd428378..4d88f8420 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -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 ( -

- You must be an administrator to access config settings. Please find - the nearest Admin and ask them to level you up! -

- ); + return

{t('configure.access_message')}

; } + const passProps = { + root, + settings, + }; + return (
+
- + {t('configure.stream_settings')} @@ -74,10 +61,7 @@ export default class Configure extends Component {
- + {React.cloneElement(this.props.children, passProps)}
); @@ -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; diff --git a/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css new file mode 100644 index 000000000..f66a92e44 --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.css @@ -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; +} \ No newline at end of file diff --git a/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js new file mode 100644 index 000000000..fca87d2cd --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/SaveChangesDialog.js @@ -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, +}) => ( + + + × + +
+ {t('configure.save_changes_dialog.unsaved_changes')} +
+ {t('configure.save_changes_dialog.copy')} +
+ + Cancel + + + +
+
+); + +SaveChangesDialog.propTypes = { + saveDialog: PropTypes.bool.isRequired, + hideSaveDialog: PropTypes.func.isRequired, + saveChanges: PropTypes.func.isRequired, + discardChanges: PropTypes.func.isRequired, +}; + +export default SaveChangesDialog; diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index ce6ea0627..9f980d31b 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -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
{this.props.data.error.message}
; @@ -30,14 +85,20 @@ class ConfigureContainer extends Component { return ( + > + {this.props.children} + ); } } @@ -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, }; diff --git a/locales/en.yml b/locales/en.yml index 08d64ddc8..6b2330f27 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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" diff --git a/locales/es.yml b/locales/es.yml index ba6cb8ad3..4e7a2c51b 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -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"