diff options
author | Joram Wilander <jwawilander@gmail.com> | 2017-06-05 12:49:38 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-05 12:49:38 -0400 |
commit | abd0466a42d6b9897ba9e3bcb373b41974e9c46f (patch) | |
tree | aee90a2041d243ed4094b7180db0642f73b57e82 /webapp/components | |
parent | 0f3bd85b8dddc6805e260a9d19cadcc603a12e17 (diff) | |
download | chat-abd0466a42d6b9897ba9e3bcb373b41974e9c46f.tar.gz chat-abd0466a42d6b9897ba9e3bcb373b41974e9c46f.tar.bz2 chat-abd0466a42d6b9897ba9e3bcb373b41974e9c46f.zip |
PLT-3466 E10: Add announcement bar feature (#6509)
* E10 - Add announcement bar feature
* Updates per feedback
* Add component tests and snapshots
* Update snapshots
* Updating color picker UI (#6543)
* Add class to body tag when banner is not dismissable and clean up localstorage items when banner changes
* Fixing links (#6544)
* Updating UI for fixed error bar (#6552)
* Truncating text on fixed banner (#6561)
* Plt 3466 - Error bar link states (#6577)
* Updating error bar hover state
* Updating error bar link states
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/admin_console/admin_console.jsx | 9 | ||||
-rw-r--r-- | webapp/components/admin_console/color_setting.jsx | 119 | ||||
-rw-r--r-- | webapp/components/admin_console/policy_settings.jsx | 92 | ||||
-rw-r--r-- | webapp/components/announcement_bar/announcement_bar.jsx (renamed from webapp/components/error_bar.jsx) | 126 | ||||
-rw-r--r-- | webapp/components/announcement_bar/index.js | 16 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_controller.jsx | 4 | ||||
-rw-r--r-- | webapp/components/create_team/create_team_controller.jsx | 4 | ||||
-rw-r--r-- | webapp/components/login/login_controller.jsx | 4 | ||||
-rw-r--r-- | webapp/components/needs_team/needs_team.jsx | 4 | ||||
-rw-r--r-- | webapp/components/select_team/select_team.jsx | 4 | ||||
-rw-r--r-- | webapp/components/signup/signup_controller.jsx | 4 |
11 files changed, 343 insertions, 43 deletions
diff --git a/webapp/components/admin_console/admin_console.jsx b/webapp/components/admin_console/admin_console.jsx index 80d9bfed9..b8250bab2 100644 --- a/webapp/components/admin_console/admin_console.jsx +++ b/webapp/components/admin_console/admin_console.jsx @@ -1,12 +1,11 @@ -import PropTypes from 'prop-types'; - // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React from 'react'; +import PropTypes from 'prop-types'; import 'bootstrap'; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import AdminStore from 'stores/admin_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -52,7 +51,7 @@ export default class AdminConsole extends React.Component { if (config && Object.keys(config).length === 0 && config.constructor === 'Object') { return ( <div className='admin-console__wrapper'> - <ErrorBar/> + <AnnouncementBar/> <div className='admin-console'/> </div> ); @@ -64,7 +63,7 @@ export default class AdminConsole extends React.Component { }); return ( <div className='admin-console__wrapper'> - <ErrorBar/> + <AnnouncementBar/> <div className='admin-console'> <AdminSidebar/> {children} diff --git a/webapp/components/admin_console/color_setting.jsx b/webapp/components/admin_console/color_setting.jsx new file mode 100644 index 000000000..483b585ee --- /dev/null +++ b/webapp/components/admin_console/color_setting.jsx @@ -0,0 +1,119 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Setting from './setting.jsx'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import {ChromePicker} from 'react-color'; + +export default class ColorSetting extends React.PureComponent { + static propTypes = { + + /* + * The unique identifer for the admin console setting + */ + id: PropTypes.string.isRequired, + + /* + * The text/jsx display name for the setting + */ + label: PropTypes.node.isRequired, + + /* + * The text/jsx help text to display underneath the setting + */ + helpText: PropTypes.node, + + /* + * The hex color value + */ + value: PropTypes.string.isRequired, + + /* + * Function called when the input changes + */ + onChange: PropTypes.func, + + /* + * Set to disable the setting + */ + disabled: PropTypes.bool + } + + constructor(props) { + super(props); + + this.state = { + showPicker: false + }; + } + + componentDidMount() { + document.addEventListener('click', this.closePicker); + } + + componentWillUnmount() { + document.removeEventListener('click', this.closePicker); + } + + handleChange = (color) => { + this.props.onChange(this.props.id, color.hex); + } + + togglePicker = () => { + if (this.props.disabled) { + this.setState({showPicker: false}); + } + this.setState({showPicker: !this.state.showPicker}); + } + + closePicker = (e) => { + if (!e.target.closest('.picker-' + this.props.id)) { + this.setState({showPicker: false}); + } + } + + onTextInput = (e) => { + this.props.onChange(this.props.id, e.target.value); + } + + render() { + let picker; + if (this.state.showPicker) { + picker = ( + <div className={'color-picker__popover picker-' + this.props.id}> + <ChromePicker + color={this.props.value} + onChange={this.handleChange} + /> + </div> + ); + } + + return ( + <Setting + label={this.props.label} + helpText={this.props.helpText} + inputId={this.props.id} + > + <div className='input-group color-picker colorpicker-element'> + <input + type='text' + className='form-control' + value={this.props.value} + onChange={this.onTextInput} + disabled={this.props.disabled} + /> + <span + className={'input-group-addon picker-' + this.props.id} + onClick={this.togglePicker} + > + <i style={{backgroundColor: this.props.value}}/> + </span> + {picker} + </div> + </Setting> + ); + } +} diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx index 7d2985001..f689efd82 100644 --- a/webapp/components/admin_console/policy_settings.jsx +++ b/webapp/components/admin_console/policy_settings.jsx @@ -8,6 +8,9 @@ import SettingsGroup from './settings_group.jsx'; import DropdownSetting from './dropdown_setting.jsx'; import RadioSetting from './radio_setting.jsx'; import PostEditSetting from './post_edit_setting.jsx'; +import BooleanSetting from './boolean_setting.jsx'; +import TextSetting from './text_setting.jsx'; +import ColorSetting from './color_setting.jsx'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -35,6 +38,11 @@ export default class PolicySettings extends AdminSettings { config.TeamSettings.RestrictPublicChannelDeletion = this.state.restrictPublicChannelDeletion; config.TeamSettings.RestrictPrivateChannelDeletion = this.state.restrictPrivateChannelDeletion; config.TeamSettings.RestrictPrivateChannelManageMembers = this.state.restrictPrivateChannelManageMembers; + config.AnnouncementSettings.EnableBanner = this.state.enableBanner; + config.AnnouncementSettings.BannerText = this.state.bannerText; + config.AnnouncementSettings.BannerColor = this.state.bannerColor; + config.AnnouncementSettings.BannerTextColor = this.state.bannerTextColor; + config.AnnouncementSettings.AllowBannerDismissal = this.state.allowBannerDismissal; return config; } @@ -51,7 +59,12 @@ export default class PolicySettings extends AdminSettings { restrictPrivateChannelManagement: config.TeamSettings.RestrictPrivateChannelManagement, restrictPublicChannelDeletion: config.TeamSettings.RestrictPublicChannelDeletion, restrictPrivateChannelDeletion: config.TeamSettings.RestrictPrivateChannelDeletion, - restrictPrivateChannelManageMembers: config.TeamSettings.RestrictPrivateChannelManageMembers + restrictPrivateChannelManageMembers: config.TeamSettings.RestrictPrivateChannelManageMembers, + enableBanner: config.AnnouncementSettings.EnableBanner, + bannerText: config.AnnouncementSettings.BannerText, + bannerColor: config.AnnouncementSettings.BannerColor, + bannerTextColor: config.AnnouncementSettings.BannerTextColor, + allowBannerDismissal: config.AnnouncementSettings.AllowBannerDismissal }; } @@ -317,6 +330,83 @@ export default class PolicySettings extends AdminSettings { /> } /> + <BooleanSetting + id='enableBanner' + label={ + <FormattedMessage + id='admin.general.policy.enableBannerTitle' + defaultMessage='Enable Announcement Banner:' + /> + } + helpText={ + <FormattedMessage + id='admin.general.policy.enableBannerDesc' + defaultMessage='Enable an announcement banner across all teams.' + /> + } + value={this.state.enableBanner} + onChange={this.handleChange} + /> + <TextSetting + id='bannerText' + label={ + <FormattedMessage + id='admin.general.policy.bannerTextTitle' + defaultMessage='Banner Text:' + /> + } + helpText={ + <FormattedMessage + id='admin.general.policy.bannerTextDesc' + defaultMessage='Text that will appear in the announcement banner.' + /> + } + value={this.state.bannerText} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + <ColorSetting + id='bannerColor' + label={ + <FormattedMessage + id='admin.general.policy.bannerColorTitle' + defaultMessage='Banner Color:' + /> + } + value={this.state.bannerColor} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + <ColorSetting + id='bannerTextColor' + label={ + <FormattedMessage + id='admin.general.policy.bannerTextColorTitle' + defaultMessage='Banner Text Color:' + /> + } + value={this.state.bannerTextColor} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + <BooleanSetting + id='allowBannerDismissal' + label={ + <FormattedMessage + id='admin.general.policy.allowBannerDismissalTitle' + defaultMessage='Allow Banner Dismissal:' + /> + } + helpText={ + <FormattedMessage + id='admin.general.policy.allowBannerDismissalDesc' + defaultMessage='When true, users can dismiss the banner until its next update. When false, the banner is permanently visible until it is turned off by the System Admin.' + /> + } + value={this.state.allowBannerDismissal} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> </SettingsGroup> ); } diff --git a/webapp/components/error_bar.jsx b/webapp/components/announcement_bar/announcement_bar.jsx index 97fbbdca0..ed097c436 100644 --- a/webapp/components/error_bar.jsx +++ b/webapp/components/announcement_bar/announcement_bar.jsx @@ -18,8 +18,19 @@ const RENEWAL_LINK = 'https://licensing.mattermost.com/renew'; const BAR_DEVELOPER_TYPE = 'developer'; const BAR_CRITICAL_TYPE = 'critical'; +const BAR_ANNOUNCEMENT_TYPE = 'announcement'; + +const ANNOUNCEMENT_KEY = 'announcement--'; + +export default class AnnouncementBar extends React.PureComponent { + static propTypes = { + + /* + * Set if the user is logged in + */ + isLoggedIn: React.PropTypes.bool.isRequired + } -export default class ErrorBar extends React.Component { constructor() { super(); @@ -31,7 +42,7 @@ export default class ErrorBar extends React.Component { this.setInitialError(); - this.state = ErrorStore.getLastError() || {}; + this.state = this.getState(); } setInitialError() { @@ -62,11 +73,38 @@ export default class ErrorBar extends React.Component { } else if (isLicenseExpired() && isSystemAdmin) { ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_EXPIRED, type: BAR_CRITICAL_TYPE}); } else if (isLicenseExpiring() && isSystemAdmin) { - ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_EXPIRING}); + ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_EXPIRING, type: BAR_CRITICAL_TYPE}); } } - isValidError(s) { + getState() { + const error = ErrorStore.getLastError(); + if (error) { + return {message: error.message, color: null, textColor: null, type: error.type, allowDismissal: true}; + } + + const bannerText = global.window.mm_config.BannerText || ''; + const allowDismissal = global.window.mm_config.AllowBannerDismissal === 'true'; + const bannerDismissed = localStorage.getItem(ANNOUNCEMENT_KEY + global.window.mm_config.BannerText); + + if (global.window.mm_config.EnableBanner === 'true' && + bannerText.length > 0 && + (!bannerDismissed || !allowDismissal)) { + // Remove old local storage items + Utils.removePrefixFromLocalStorage(ANNOUNCEMENT_KEY); + return { + message: bannerText, + color: global.window.mm_config.BannerColor, + textColor: global.window.mm_config.BannerTextColor, + type: BAR_ANNOUNCEMENT_TYPE, + allowDismissal + }; + } + + return {message: null, color: null, colorText: null, textColor: null, type: null, allowDismissal: true}; + } + + isValidState(s) { if (!s) { return false; } @@ -83,28 +121,40 @@ export default class ErrorBar extends React.Component { } componentDidMount() { + if (this.props.isLoggedIn && !this.state.allowDismissal) { + document.body.classList.add('error-bar--fixed'); + } + ErrorStore.addChangeListener(this.onErrorChange); AnalyticsStore.addChangeListener(this.onAnalyticsChange); } componentWillUnmount() { + document.body.classList.remove('error-bar--fixed'); ErrorStore.removeChangeListener(this.onErrorChange); AnalyticsStore.removeChangeListener(this.onAnalyticsChange); } - onErrorChange() { - var newState = ErrorStore.getLastError(); + componentDidUpdate(prevProps, prevState) { + if (!this.props.isLoggedIn) { + return; + } - if (newState) { - if (newState.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) { - AsyncClient.getStandardAnalytics(); - } - this.setState(newState); - } else { - this.setState({message: null}); + if (!prevState.allowDismissal && this.state.allowDismissal) { + document.body.classList.remove('error-bar--fixed'); + } else if (prevState.allowDismissal && !this.state.allowDismissal) { + document.body.classList.add('error-bar--fixed'); } } + onErrorChange() { + const newState = this.getState(); + if (newState.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) { + AsyncClient.getStandardAnalytics(); + } + this.setState(newState); + } + onAnalyticsChange() { const stats = AnalyticsStore.getAllSystem(); this.setState({totalUsers: stats[StatTypes.TOTAL_USERS]}); @@ -115,28 +165,57 @@ export default class ErrorBar extends React.Component { e.preventDefault(); } + if (this.state.type === BAR_ANNOUNCEMENT_TYPE) { + localStorage.setItem(ANNOUNCEMENT_KEY + this.state.message, true); + } + if (ErrorStore.getLastError() && ErrorStore.getLastError().notification) { ErrorStore.clearNotificationError(); } else { ErrorStore.clearLastError(); } - this.setState({message: null}); + this.setState(this.getState()); } render() { - if (!this.isValidError(this.state)) { + if (!this.isValidState(this.state)) { return <div/>; } - var errClass = 'error-bar'; + if (!this.props.isLoggedIn && this.state.type === BAR_ANNOUNCEMENT_TYPE) { + return <div/>; + } - if (this.state.type === BAR_DEVELOPER_TYPE) { + let errClass = 'error-bar'; + let dismissClass = ' error-bar--fixed'; + const barStyle = {}; + const linkStyle = {}; + if (this.state.color && this.state.textColor) { + barStyle.backgroundColor = this.state.color; + barStyle.color = this.state.textColor; + linkStyle.color = this.state.textColor; + } else if (this.state.type === BAR_DEVELOPER_TYPE) { errClass = 'error-bar-developer'; } else if (this.state.type === BAR_CRITICAL_TYPE) { errClass = 'error-bar-critical'; } + let closeButton; + if (this.state.allowDismissal) { + dismissClass = ''; + closeButton = ( + <a + href='#' + className='error-bar__close' + style={linkStyle} + onClick={this.handleClose} + > + {'×'} + </a> + ); + } + const renewalLink = RENEWAL_LINK + '?id=' + global.window.mm_license.Id + '&user_count=' + this.state.totalUsers; let message = this.state.message; @@ -217,15 +296,12 @@ export default class ErrorBar extends React.Component { } return ( - <div className={errClass}> + <div + className={errClass + dismissClass} + style={barStyle} + > <span>{message}</span> - <a - href='#' - className='error-bar__close' - onClick={this.handleClose} - > - {'×'} - </a> + {closeButton} </div> ); } diff --git a/webapp/components/announcement_bar/index.js b/webapp/components/announcement_bar/index.js new file mode 100644 index 000000000..8fe4f56b4 --- /dev/null +++ b/webapp/components/announcement_bar/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; + +import AnnouncementBar from './announcement_bar.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + isLoggedIn: Boolean(getCurrentUserId(state)) + }; +} + +export default connect(mapStateToProps)(AnnouncementBar); diff --git a/webapp/components/backstage/backstage_controller.jsx b/webapp/components/backstage/backstage_controller.jsx index 4619641b1..795fb0e95 100644 --- a/webapp/components/backstage/backstage_controller.jsx +++ b/webapp/components/backstage/backstage_controller.jsx @@ -10,7 +10,7 @@ import UserStore from 'stores/user_store.jsx'; import BackstageSidebar from './components/backstage_sidebar.jsx'; import BackstageNavbar from './components/backstage_navbar.jsx'; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; export default class BackstageController extends React.Component { static get propTypes() { @@ -55,7 +55,7 @@ export default class BackstageController extends React.Component { render() { return ( <div className='backstage'> - <ErrorBar/> + <AnnouncementBar/> <BackstageNavbar team={this.state.team}/> <div className='backstage-body'> <BackstageSidebar diff --git a/webapp/components/create_team/create_team_controller.jsx b/webapp/components/create_team/create_team_controller.jsx index e06ea9465..1e2e7dde1 100644 --- a/webapp/components/create_team/create_team_controller.jsx +++ b/webapp/components/create_team/create_team_controller.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -63,7 +63,7 @@ export default class CreateTeamController extends React.Component { return ( <div> - <ErrorBar/> + <AnnouncementBar/> <div className='signup-header'> <Link to={url}> <span className='fa fa-chevron-left'/> diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index cef7fe435..212a09bf2 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import LoginMfa from './components/login_mfa.jsx'; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import FormError from 'components/form_error.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -621,7 +621,7 @@ export default class LoginController extends React.Component { return ( <div> - <ErrorBar/> + <AnnouncementBar/> <div className='col-sm-12'> <div className={'signup-team__container ' + customClass}> <div className='signup__markdown'> diff --git a/webapp/components/needs_team/needs_team.jsx b/webapp/components/needs_team/needs_team.jsx index aa3ea46e8..75ec40653 100644 --- a/webapp/components/needs_team/needs_team.jsx +++ b/webapp/components/needs_team/needs_team.jsx @@ -23,7 +23,7 @@ import Constants from 'utils/constants.jsx'; const TutorialSteps = Constants.TutorialSteps; const Preferences = Constants.Preferences; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import SidebarRight from 'components/sidebar_right.jsx'; import SidebarRightMenu from 'components/sidebar_right_menu.jsx'; import Navbar from 'components/navbar.jsx'; @@ -211,7 +211,7 @@ export default class NeedsTeam extends React.Component { return ( <div className='channel-view'> - <ErrorBar/> + <AnnouncementBar/> <WebrtcNotification/> <div className='container-fluid'> <SidebarRight channel={channel}/> diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx index 858329bd0..fe706af0f 100644 --- a/webapp/components/select_team/select_team.jsx +++ b/webapp/components/select_team/select_team.jsx @@ -5,7 +5,7 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import LoadingScreen from 'components/loading_screen.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import SelectTeamItem from './components/select_team_item.jsx'; @@ -218,7 +218,7 @@ export default class SelectTeam extends React.Component { } return ( <div> - <ErrorBar/> + <AnnouncementBar/> <div className='signup-header'> {headerButton} </div> diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx index 5a9e535a8..e9024a389 100644 --- a/webapp/components/signup/signup_controller.jsx +++ b/webapp/components/signup/signup_controller.jsx @@ -18,7 +18,7 @@ import {addUserToTeamFromInvite, getInviteInfo} from 'actions/team_actions.jsx'; import {loadMe} from 'actions/user_actions.jsx'; import logoImage from 'images/logo.png'; -import ErrorBar from 'components/error_bar.jsx'; +import AnnouncementBar from 'components/announcement_bar'; import {FormattedMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router/es6'; @@ -319,7 +319,7 @@ export default class SignupController extends React.Component { return ( <div> - <ErrorBar/> + <AnnouncementBar/> <div className='signup-header'> <Link to='/'> <span className='fa fa-chevron-left'/> |