Merge branch 'master' into master

This commit is contained in:
Kim Gardner
2018-04-23 10:23:57 -04:00
committed by GitHub
29 changed files with 467 additions and 17 deletions
+2
View File
@@ -10,6 +10,7 @@ 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 OrganizationSettings from './routes/Configure/containers/OrganizationSettings';
import { ModerationLayout, Moderation } from 'routes/Moderation';
@@ -25,6 +26,7 @@ const routes = (
<Route path="stream" component={StreamSettings} />
<Route path="moderation" component={ModerationSettings} />
<Route path="tech" component={TechSettings} />
<Route path="organization" component={OrganizationSettings} />
<IndexRedirect to="stream" />
</Route>
@@ -5,6 +5,7 @@ const initialState = {
isLoading: false,
data: {
settings: {
organizationContactEmail: '',
organizationName: '',
domains: {
whitelist: [],
@@ -19,6 +20,7 @@ const initialState = {
},
errors: {
organizationName: '',
organizationContactEmail: '',
username: '',
email: '',
password: '',
@@ -8,7 +8,14 @@ import SaveChangesDialog from './SaveChangesDialog';
class Configure extends React.Component {
render() {
const { canSave, currentUser, root, savePending, settings } = this.props;
const {
canSave,
currentUser,
root,
savePending,
settings,
clearPending,
} = this.props;
if (!can(currentUser, 'UPDATE_CONFIG')) {
return <p>{t('configure.access_message')}</p>;
@@ -17,6 +24,9 @@ class Configure extends React.Component {
const passProps = {
root,
settings,
savePending,
clearPending,
canSave,
};
return (
@@ -41,6 +51,9 @@ class Configure extends React.Component {
<Item itemId="tech" icon="code">
{t('configure.tech_settings')}
</Item>
<Item itemId="organization" icon="people">
{t('configure.organization_information')}
</Item>
</List>
<div className={styles.saveBox}>
{canSave ? (
@@ -81,6 +94,7 @@ Configure.propTypes = {
children: PropTypes.node.isRequired,
saveDialog: PropTypes.bool,
hideSaveDialog: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
};
export default Configure;
@@ -0,0 +1,87 @@
.label {
display: block;
}
.detailList {
padding: 0;
margin: 0;
}
.detailLabel {
color: #000;
font-size: 1.1em;
font-weight: bold;
display: block;
margin-bottom: 4px;
}
.detailValue {
padding: 6px 0;
border: solid 1px transparent;
display: block;
font-size: 1.1em;
border-radius: 2px;
color: #424242;
box-sizing: border-box;
min-width: 300px;
}
.editable {
padding-left: 10px;
padding-right: 10px;
border-color: grey;
}
.detailItem {
margin-bottom: 16px;
list-style: none;
}
.button, .button:disabled {
background: white;
border: solid 1px grey;
}
.actionBox {
flex-grow: 0;
}
.cancelButton {
padding: 10px;
display: block;
color: #4f5c67;
font-weight: 500;
&:hover {
cursor: pointer;
}
}
.changedSave {
background-color: #00796B;
color: white;
}
.errorList {
list-style: none;
padding: 0;
margin: 0;
}
.errorItem {
padding: 5px 10px;
margin-bottom: 20px;
color: #b71c1c;
border-radius: 2px;
display: inline-block;
background: #F9D3CE;
}
.container {
display: flex;
}
.content {
flex-grow: 1;
padding-right: 40px;
}
@@ -0,0 +1,187 @@
import React from 'react';
import cn from 'classnames';
import { Button } from 'coral-ui';
import PropTypes from 'prop-types';
import styles from './OrganizationSettings.css';
import Slot from 'coral-framework/components/Slot';
import t from 'coral-framework/services/i18n';
import ConfigurePage from './ConfigurePage';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
class OrganizationSettings extends React.Component {
state = { editing: false, errors: [] };
addError = err => {
if (this.state.errors.indexOf(err) === -1) {
this.setState(({ errors }) => ({
errors: errors.concat(err),
}));
}
};
removeError = err => {
this.setState(({ errors }) => ({
errors: errors.filter(i => i !== err),
}));
};
toggleEditing = () => {
this.setState(({ editing }) => ({
editing: !editing,
}));
};
disableEditing = () => {
this.setState(() => ({
editing: false,
}));
};
updateName = event => {
const updater = { organizationName: { $set: event.target.value } };
this.props.updatePending({ updater });
};
updateEmail = event => {
let error = null;
const email = event.target.value;
// Add a blocker error
if (!validate.email(email)) {
error = true;
this.addError('email');
} else {
this.removeError('email');
}
const updater = { organizationContactEmail: { $set: email } };
const errorUpdater = { organizationEmail: { $set: error } };
this.props.updatePending({ updater, errorUpdater });
};
cancelEditing = () => {
this.disableEditing();
this.props.clearPending();
};
save = async () => {
await this.props.savePending();
this.disableEditing();
};
displayErrors = (errors = []) => (
<ul className={styles.errorList}>
{errors.map((errKey, i) => (
<li key={`${i}_${errKey}`} className={styles.errorItem}>
{errorMsj[errKey]}
</li>
))}
</ul>
);
render() {
const { settings, slotPassthrough, canSave } = this.props;
const hasErrors = this.state.errors.length;
return (
<ConfigurePage title={t('configure.organization_information')}>
<p>{t('configure.organization_info_copy')}</p>
<p>{t('configure.organization_info_copy_2')}</p>
<ConfigureCard>
<div className={styles.container}>
<div className={styles.content}>
{this.displayErrors(this.state.errors)}
<ul className={styles.detailList}>
<li className={styles.detailItem}>
<label
className={styles.detailLabel}
id={t('configure.organization_name')}
>
{t('configure.organization_name')}
</label>
<input
type="text"
className={cn(styles.detailValue, {
[styles.editable]: this.state.editing,
})}
onChange={this.updateName}
value={settings.organizationName}
id={t('configure.organization_name')}
readOnly={!this.state.editing}
/>
</li>
<li className={styles.detailItem}>
<label
className={styles.detailLabel}
id={t('configure.organization_contact_email')}
>
{t('configure.organization_contact_email')}
</label>
<input
type="text"
className={cn(styles.detailValue, {
[styles.editable]: this.state.editing,
})}
onChange={this.updateEmail}
value={settings.organizationContactEmail}
id={t('configure.organization_contact_email')}
readOnly={!this.state.editing}
/>
</li>
</ul>
</div>
{!this.state.editing ? (
<div className={styles.actionBox}>
<Button
className={styles.button}
icon="settings"
onClick={this.toggleEditing}
full
>
{t('configure.edit_info')}
</Button>
</div>
) : (
<div className={styles.actionBox}>
{canSave && !hasErrors ? (
<Button
raised
onClick={this.save}
className={styles.changedSave}
icon="check"
full
>
{t('configure.save')}
</Button>
) : (
<Button className={styles.button} disabled icon="check" full>
{t('configure.save')}
</Button>
)}
<a className={styles.cancelButton} onClick={this.cancelEditing}>
{t('cancel')}
</a>
</div>
)}
</div>
</ConfigureCard>
<Slot fill="adminOrganizationSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
}
OrganizationSettings.propTypes = {
savePending: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
slotPassthrough: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
};
export default OrganizationSettings;
@@ -16,6 +16,7 @@ import {
hideSaveDialog,
} from '../../../actions/configure';
import Configure from '../components/Configure';
import OrganizationSettings from './OrganizationSettings';
import { withRouter } from 'react-router';
class ConfigureContainer extends React.Component {
@@ -83,18 +84,21 @@ class ConfigureContainer extends React.Component {
return <Spinner />;
}
const activeSection = this.props.routes[3].path;
return (
<Configure
saveChanges={this.saveChanges}
discardChanges={this.discardChanges}
saveDialog={this.props.saveDialog}
activeSection={this.props.routes[3].path}
activeSection={activeSection}
hideSaveDialog={this.props.hideSaveDialog}
canSave={this.props.canSave}
currentUser={this.props.currentUser}
root={this.props.root}
settings={this.props.mergedSettings}
handleSectionChange={this.handleSectionChange}
clearPending={this.props.clearPending}
savePending={this.savePending}
>
{this.props.children}
@@ -110,10 +114,12 @@ const withConfigureQuery = withQuery(
...${getDefinitionName(StreamSettings.fragments.settings)}
...${getDefinitionName(TechSettings.fragments.settings)}
...${getDefinitionName(ModerationSettings.fragments.settings)}
...${getDefinitionName(OrganizationSettings.fragments.settings)}
}
...${getDefinitionName(StreamSettings.fragments.root)}
...${getDefinitionName(TechSettings.fragments.root)}
...${getDefinitionName(ModerationSettings.fragments.root)}
...${getDefinitionName(OrganizationSettings.fragments.root)}
}
${StreamSettings.fragments.root}
${StreamSettings.fragments.settings}
@@ -121,6 +127,8 @@ const withConfigureQuery = withQuery(
${TechSettings.fragments.settings}
${ModerationSettings.fragments.root}
${ModerationSettings.fragments.settings}
${OrganizationSettings.fragments.root}
${OrganizationSettings.fragments.settings}
`,
{
options: () => ({
@@ -0,0 +1,53 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import OrganizationSettings from '../components/OrganizationSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
import { mapProps } from 'recompose';
const slots = ['adminOrganizationSettings'];
const mapStateToProps = state => ({
errors: state.configure.errors,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
updatePending,
},
dispatch
);
export default compose(
withFragments({
root: gql`
fragment TalkAdmin_OrganizationSettings_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
settings: gql`
fragment TalkAdmin_OrganizationSettings_settings on Settings {
organizationName
organizationContactEmail
${getSlotFragmentSpreads(slots, 'settings')}
}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
settings,
updatePending,
errors,
},
updatePending,
settings,
errors,
...rest,
}))
)(OrganizationSettings);
@@ -1,15 +1,16 @@
import React, { Component } from 'react';
import React from 'react';
import styles from './Install.css';
import { Wizard, WizardNav } from 'coral-ui';
import Layout from 'coral-admin/src/components/Layout';
import PropTypes from 'prop-types';
import InitialStep from './Steps/InitialStep';
import AddOrganizationName from './Steps/AddOrganizationName';
import OrganizationDetails from './Steps/OrganizationDetails';
import CreateYourAccount from './Steps/CreateYourAccount';
import PermittedDomainsStep from './Steps/PermittedDomainsStep';
import FinalStep from './Steps/FinalStep';
export default class Install extends Component {
class Install extends React.Component {
handleDomainsChange = value => {
this.props.updatePermittedDomains(value);
};
@@ -55,7 +56,7 @@ export default class Install extends Component {
goToStep={this.props.goToStep}
>
<InitialStep />
<AddOrganizationName
<OrganizationDetails
install={install}
handleSettingsChange={this.handleSettingsChange}
handleSettingsSubmit={this.handleSettingsSubmit}
@@ -81,3 +82,18 @@ export default class Install extends Component {
);
}
}
Install.propTypes = {
updatePermittedDomains: PropTypes.func.isRequired,
updateSettingsFormData: PropTypes.func.isRequired,
updateUserFormData: PropTypes.func.isRequired,
submitSettings: PropTypes.func.isRequired,
submitUser: PropTypes.func.isRequired,
install: PropTypes.object.isRequired,
nextStep: PropTypes.func.isRequired,
previousStep: PropTypes.func.isRequired,
goToStep: PropTypes.func.isRequired,
finishInstall: PropTypes.func.isRequired,
};
export default Install;
@@ -21,6 +21,17 @@ const AddOrganizationName = props => {
showErrors={install.showErrors}
errorMsg={install.errors.organizationName}
/>
<TextField
className={styles.TextField}
id="organizationContactEmail"
type="email"
label={t('install.create.organization_contact_email')}
onChange={handleSettingsChange}
showErrors={install.showErrors}
errorMsg={install.errors.organizationContactEmail}
/>
<Button
className="talk-install-step-2-save-button"
type="submit"
@@ -1,8 +1,7 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose } from 'react-apollo';
import Install from '../components/Install';
import {
@@ -18,7 +17,7 @@ import {
updatePermittedDomains,
} from '../../../actions/install';
class InstallContainer extends Component {
class InstallContainer extends React.Component {
componentDidMount() {
const { checkInstall } = this.props;
checkInstall(() => {
@@ -27,7 +26,21 @@ class InstallContainer extends Component {
}
render() {
return <Install {...this.props} />;
return (
<Install
install={this.props.install}
goToStep={this.props.goToStep}
nextStep={this.props.nextStep}
submitUser={this.props.submitUser}
checkInstall={this.props.checkInstall}
previousStep={this.props.previousStep}
finishInstall={this.props.finishInstall}
submitSettings={this.props.submitSettings}
updateUserFormData={this.props.updateUserFormData}
updateSettingsFormData={this.props.updateSettingsFormData}
updatePermittedDomains={this.props.updatePermittedDomains}
/>
);
}
}
@@ -35,6 +48,20 @@ InstallContainer.contextTypes = {
router: PropTypes.object,
};
InstallContainer.propTypes = {
install: PropTypes.object.isRequired,
goToStep: PropTypes.func.isRequired,
nextStep: PropTypes.func.isRequired,
submitUser: PropTypes.func.isRequired,
checkInstall: PropTypes.func.isRequired,
previousStep: PropTypes.func.isRequired,
finishInstall: PropTypes.func.isRequired,
submitSettings: PropTypes.func.isRequired,
updateUserFormData: PropTypes.func.isRequired,
updateSettingsFormData: PropTypes.func.isRequired,
updatePermittedDomains: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
install: state.install,
});
@@ -56,6 +83,4 @@ const mapDispatchToProps = dispatch =>
dispatch
);
export default compose(connect(mapStateToProps, mapDispatchToProps))(
InstallContainer
);
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
@@ -43,7 +43,7 @@ const ConfigureCard = ({
);
ConfigureCard.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.string,
className: PropTypes.string,
onCheckbox: PropTypes.func,
checked: PropTypes.bool,
+1
View File
@@ -6,4 +6,5 @@ export default {
username: t('error.username'),
confirmPassword: t('error.confirm_password'),
organizationName: t('error.organization_name'),
organizationContactEmail: t('error.organization_contact_email'),
};
@@ -4,4 +4,5 @@ export default {
confirmPassword: () => true,
username: username => /^[a-zA-Z0-9_]+$/.test(username),
organizationName: org => /^[a-zA-Z0-9_ ]+$/.test(org),
organizationContactEmail: email => /^.+@.+\..+$/.test(email),
};
+1 -1
View File
@@ -1,5 +1,5 @@
.root {
vertical-align: middle;
vertical-align: sub;
font-size: inherit;
}