Display digest settings

This commit is contained in:
Chi Vinh Le
2018-03-07 14:38:53 +01:00
parent 033c6464e9
commit e6b28a55af
22 changed files with 218 additions and 22 deletions
@@ -130,3 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
.loadMore {
margin-top: 24px;
}
.roleDropdown {
width: 150px;
}
.roleOption {
min-width: 100px;
}
@@ -200,24 +200,31 @@ class People extends React.Component {
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
className={cn(
'talk-admin-community-people-dd-role',
styles.roleDropdown
)}
value={user.role}
placeholder={t('community.role')}
onChange={role => setUserRole(user.id, role)}
>
<Option
className={styles.roleOption}
value={COMMENTER}
label={t('community.commenter')}
/>
<Option
className={styles.roleOption}
value={STAFF}
label={t('community.staff')}
/>
<Option
className={styles.roleOption}
value={MODERATOR}
label={t('community.moderator')}
/>
<Option
className={styles.roleOption}
value={ADMIN}
label={t('community.admin')}
/>
@@ -89,3 +89,12 @@
display: inline-block;
}
}
.statusDropdown {
width: 150px;
}
.statusDropdownOption {
min-width: 100px;
}
@@ -22,11 +22,20 @@ class Stories extends Component {
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
return (
<Dropdown
className={styles.statusDropdown}
value={closed}
onChange={value => this.props.onStatusChange(value, id)}
>
<Option value={false} label={t('streams.open')} />
<Option value={true} label={t('streams.closed')} />
<Option
value={false}
label={t('streams.open')}
className={styles.statusDropdownOption}
/>
<Option
value={true}
label={t('streams.closed')}
className={styles.statusDropdownOption}
/>
</Dropdown>
);
};
@@ -10,6 +10,7 @@ class TalkProvider extends React.Component {
plugins: this.props.plugins,
rest: this.props.rest,
graphql: this.props.graphql,
introspection: this.props.introspection,
notification: this.props.notification,
localStorage: this.props.localStorage,
sessionStorage: this.props.sessionStorage,
@@ -29,6 +30,7 @@ class TalkProvider extends React.Component {
TalkProvider.childContextTypes = {
pym: PropTypes.object,
introspection: PropTypes.object,
eventEmitter: PropTypes.object,
plugins: PropTypes.object,
rest: PropTypes.func,
+1
View File
@@ -11,6 +11,7 @@ export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export { default as withPopupAuthHandler } from './withPopupAuthHandler';
export { default as withEnumValues } from './withEnumValues';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
@@ -0,0 +1,24 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
/**
* WithEnumValues inject given property name for given enum.
*/
export default (enumName, propName) =>
hoistStatics(WrappedComponent => {
class WithEnumValues extends React.Component {
static contextTypes = {
introspection: PropTypes.object,
};
render() {
const inject = {
[propName]: this.context.introspection.getEnumValues(enumName),
};
return <WrappedComponent {...this.props} {...inject} />;
}
}
return WithEnumValues;
});
@@ -19,6 +19,16 @@ class Introspection {
isValidEnumValue(name, value) {
return this._enums[name] && this._enums[name].indexOf(value) >= 0;
}
/**
* getEnumValues returns array of possible values.
* @param {string} name
* @param {string} value
* @return {array}
*/
getEnumValues(name) {
return this._enums[name];
}
}
/**
+10 -4
View File
@@ -1,20 +1,27 @@
.dropdown {
position: relative;
width: 150px;
height: 34px;
background: #2c2c2c;
box-sizing: border-box;
color: white;
border-radius: 3px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
line-height: 20px;
font-size: 0.98em;
border-radius: 3px;
cursor: pointer;
&.disabled {
color: #e5e5e5;
background: #888;
cursor: default;
pointer-events: none;
}
}
.toggle {
padding: 8px 15px;
cursor: pointer;
padding: 8px 45px 8px 15px;
outline: none;
&:focus {
@@ -38,7 +45,6 @@
border-radius: 2px;
background: white;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
width: 100%;
list-style: none;
padding: 0;
margin: 0;
+24 -2
View File
@@ -13,6 +13,12 @@ class Dropdown extends React.Component {
isOpen: false,
};
componentWillReceiveProps(nextProps) {
if (this.state.isOpen && nextProps.disabled) {
this.toggle();
}
}
componentDidUpdate(_, prevState) {
if (!this.state.isOpen && prevState.isOpen) {
// Refocus on the toggle element when menu closes.
@@ -69,6 +75,10 @@ class Dropdown extends React.Component {
};
toggle = () => {
if (this.props.disabled) {
return false;
}
this.setState({
isOpen: !this.state.isOpen,
});
@@ -135,11 +145,21 @@ class Dropdown extends React.Component {
containerClassName,
toggleClassName,
toggleOpenClassName,
disabled,
className,
} = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div
className={cn(styles.dropdown, containerClassName, 'dd dd-container')}
className={cn(
styles.dropdown,
className,
containerClassName,
'dd dd-container',
{
[styles.disabled]: disabled,
}
)}
>
<div
className={cn(styles.toggle, toggleClassName, {
@@ -150,7 +170,7 @@ class Dropdown extends React.Component {
role="button"
aria-pressed={this.state.isOpen}
aria-haspopup="true"
tabIndex="0"
tabIndex={disabled ? '-1' : '0'}
ref={this.handleToggleRef}
>
{this.props.icon && (
@@ -206,6 +226,7 @@ class Dropdown extends React.Component {
}
Dropdown.propTypes = {
className: PropTypes.string,
containerClassName: PropTypes.string,
toggleClassName: PropTypes.string,
toggleOpenClassName: PropTypes.string,
@@ -213,6 +234,7 @@ Dropdown.propTypes = {
icon: PropTypes.string,
onChange: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
disabled: PropTypes.bool,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
+1
View File
@@ -2,6 +2,7 @@
padding: 10px;
text-transform: capitalize;
outline: none;
white-space: nowrap;
&:focus, &:hover {
background-color: #ccc;
+1
View File
@@ -12,6 +12,7 @@ export {
withSignUp,
withResendEmailConfirmation,
withSetUsername,
withEnumValues,
} from 'coral-framework/hocs';
export {
withIgnoreUser,
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,5 @@
import translations from './translations.yml';
export default {
translations,
};
@@ -0,0 +1,4 @@
en:
talk-plugin-notifications:
digest_enum:
DAILY: In a daily digest
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,5 @@
import translations from './translations.yml';
export default {
translations,
};
@@ -0,0 +1,4 @@
en:
talk-plugin-notifications:
digest_enum:
HOURLY: In an hourly digest
@@ -30,3 +30,16 @@
.disabled {
color: #e5e5e5;
}
.digest {
display: flex;
align-items: center;
}
.titleDigest {
width: 100%;
}
.digestDropDown {
flex-shrink: 0;
}
@@ -4,7 +4,11 @@ import { IfSlotIsNotEmpty } from 'plugin-api/beta/client/components';
import { Slot } from 'plugin-api/beta/client/components';
import { t } from 'plugin-api/beta/client/services';
import styles from './Settings.css';
import { BareButton } from 'plugin-api/beta/client/components/ui';
import {
BareButton,
Dropdown,
Option,
} from 'plugin-api/beta/client/components/ui';
import EmailVerificationBanner from '../containers/EmailVerificationBanner';
import cn from 'classnames';
@@ -24,9 +28,13 @@ class Settings extends React.Component {
setTurnOffInputFragment,
updateNotificationSettings,
turnOffAll,
turnOffButtonDisabled,
needEmailVerification,
email,
digestFrequencyValues,
digestFrequency,
disableDigest,
disableTurnoffButton,
onChangeDigestFrequency,
} = this.props;
return (
@@ -51,14 +59,37 @@ class Settings extends React.Component {
updateNotificationSettings={updateNotificationSettings}
disabled={needEmailVerification}
/>
<BareButton
className={styles.turnOffButton}
onClick={turnOffAll}
disabled={turnOffButtonDisabled}
>
{t('talk-plugin-notifications.turn_off_all')}
</BareButton>
</div>
<div className={styles.digest}>
<h4
className={cn(styles.titleDigest, {
[styles.disabled]: disableDigest,
})}
>
{t('talk-plugin-notifications.digest_option')}
</h4>
<Dropdown
className={styles.digestDropDown}
value={digestFrequency}
onChange={onChangeDigestFrequency}
disabled={disableDigest}
>
{digestFrequencyValues.map(v => (
<Option
value={v}
key={v}
label={t(`talk-plugin-notifications.digest_enum.${v}`)}
/>
))}
</Dropdown>
</div>
<BareButton
className={styles.turnOffButton}
onClick={turnOffAll}
disabled={disableTurnoffButton}
>
{t('talk-plugin-notifications.turn_off_all')}
</BareButton>
</div>
</IfSlotIsNotEmpty>
);
@@ -72,9 +103,13 @@ Settings.propTypes = {
setTurnOffInputFragment: PropTypes.func.isRequired,
updateNotificationSettings: PropTypes.func.isRequired,
turnOffAll: PropTypes.func.isRequired,
turnOffButtonDisabled: PropTypes.bool.isRequired,
disableTurnoffButton: PropTypes.bool.isRequired,
disableDigest: PropTypes.bool.isRequired,
needEmailVerification: PropTypes.bool.isRequired,
email: PropTypes.string,
digestFrequencyValues: PropTypes.array.isRequired,
digestFrequency: PropTypes.string.isRequired,
onChangeDigestFrequency: PropTypes.func.isRequired,
};
export default Settings;
@@ -2,7 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose, gql } from 'react-apollo';
import Settings from '../components/Settings';
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import {
withFragments,
excludeIf,
withEnumValues,
} from 'plugin-api/beta/client/hocs';
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
import { withUpdateNotificationSettings } from '../mutations';
@@ -39,6 +43,11 @@ class SettingsContainer extends React.Component {
);
}
setDigestFrequency(val) {
// TODO: implement mutation.
console.log(val);
}
render() {
return (
<Settings
@@ -49,9 +58,15 @@ class SettingsContainer extends React.Component {
setTurnOffInputFragment={this.setTurnOffInputFragment}
updateNotificationSettings={this.props.updateNotificationSettings}
turnOffAll={this.turnOffAll}
turnOffButtonDisabled={this.state.hasNotifications.length === 0}
needEmailVerification={this.getNeedEmailVerification()}
email={this.props.root.me.email}
digestFrequencyValues={this.props.digestFrequencyValues}
digestFrequency={
this.props.root.me.notificationSettings.digestFrequency
}
disableDigest={this.state.hasNotifications.length === 0}
disableTurnoffButton={this.state.hasNotifications.length === 0}
onChangeDigestFrequency={this.setDigestFrequency}
/>
);
}
@@ -61,6 +76,7 @@ SettingsContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
updateNotificationSettings: PropTypes.func.isRequired,
digestFrequencyValues: PropTypes.array.isRequired,
};
const enhance = compose(
@@ -70,6 +86,9 @@ const enhance = compose(
__typename
${getSlotFragmentSpreads(slots, 'root')}
me {
notificationSettings {
digestFrequency
}
email
profiles {
provider
@@ -85,7 +104,8 @@ const enhance = compose(
props =>
!props.root.me.profiles.some(profile => profile.provider === 'local')
),
withUpdateNotificationSettings
withUpdateNotificationSettings,
withEnumValues('DIGEST_FREQUENCY', 'digestFrequencyValues')
);
export default enhance(SettingsContainer);
@@ -13,3 +13,6 @@ en:
banner_error:
title: Error
text: There has been an error sending your verification email. Please try again later.
digest_option: I want to receive notifications
digest_enum:
NONE: Immediately