From abd0466a42d6b9897ba9e3bcb373b41974e9c46f Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 5 Jun 2017 12:49:38 -0400 Subject: 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 --- webapp/components/admin_console/admin_console.jsx | 9 +- webapp/components/admin_console/color_setting.jsx | 119 ++++++++ .../components/admin_console/policy_settings.jsx | 92 +++++- .../announcement_bar/announcement_bar.jsx | 308 +++++++++++++++++++++ webapp/components/announcement_bar/index.js | 16 ++ .../components/backstage/backstage_controller.jsx | 4 +- .../create_team/create_team_controller.jsx | 4 +- webapp/components/error_bar.jsx | 232 ---------------- webapp/components/login/login_controller.jsx | 4 +- webapp/components/needs_team/needs_team.jsx | 4 +- webapp/components/select_team/select_team.jsx | 4 +- webapp/components/signup/signup_controller.jsx | 4 +- webapp/i18n/en.json | 8 + webapp/package.json | 1 + webapp/root.jsx | 16 +- webapp/sass/base/_structure.scss | 4 + webapp/sass/components/_error-bar.scss | 21 +- webapp/sass/layout/_sidebar-left.scss | 4 + webapp/sass/layout/_sidebar-right.scss | 8 +- webapp/sass/routes/_admin-console.scss | 7 + .../__snapshots__/color_setting.test.jsx.snap | 95 +++++++ .../admin_console/color_setting.test.jsx | 55 ++++ webapp/utils/utils.jsx | 13 + webapp/yarn.lock | 32 ++- 24 files changed, 801 insertions(+), 263 deletions(-) create mode 100644 webapp/components/admin_console/color_setting.jsx create mode 100644 webapp/components/announcement_bar/announcement_bar.jsx create mode 100644 webapp/components/announcement_bar/index.js delete mode 100644 webapp/components/error_bar.jsx create mode 100644 webapp/tests/components/admin_console/__snapshots__/color_setting.test.jsx.snap create mode 100644 webapp/tests/components/admin_console/color_setting.test.jsx (limited to 'webapp') 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 (
- +
); @@ -64,7 +63,7 @@ export default class AdminConsole extends React.Component { }); return (
- +
{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 = ( +
+ +
+ ); + } + + return ( + +
+ + + + + {picker} +
+
+ ); + } +} 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 { /> } /> + + } + helpText={ + + } + value={this.state.enableBanner} + onChange={this.handleChange} + /> + + } + helpText={ + + } + value={this.state.bannerText} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + + } + value={this.state.bannerColor} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + + } + value={this.state.bannerTextColor} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> + + } + helpText={ + + } + value={this.state.allowBannerDismissal} + onChange={this.handleChange} + disabled={!this.state.enableBanner} + /> ); } diff --git a/webapp/components/announcement_bar/announcement_bar.jsx b/webapp/components/announcement_bar/announcement_bar.jsx new file mode 100644 index 000000000..ed097c436 --- /dev/null +++ b/webapp/components/announcement_bar/announcement_bar.jsx @@ -0,0 +1,308 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {Link} from 'react-router'; + +import AnalyticsStore from 'stores/analytics_store.jsx'; +import ErrorStore from 'stores/error_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {ErrorBarTypes, StatTypes} from 'utils/constants.jsx'; +import {isLicenseExpiring, isLicenseExpired, isLicensePastGracePeriod, displayExpiryDate} from 'utils/license_utils.jsx'; +import * as Utils from 'utils/utils.jsx'; + +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 + } + + constructor() { + super(); + + this.onErrorChange = this.onErrorChange.bind(this); + this.onAnalyticsChange = this.onAnalyticsChange.bind(this); + this.handleClose = this.handleClose.bind(this); + + ErrorStore.clearLastError(); + + this.setInitialError(); + + this.state = this.getState(); + } + + setInitialError() { + let isSystemAdmin = false; + const user = UserStore.getCurrentUser(); + if (user) { + isSystemAdmin = Utils.isSystemAdmin(user.roles); + } + + const errorIgnored = ErrorStore.getIgnoreNotification(); + + if (!errorIgnored) { + if (isSystemAdmin && global.mm_config.SiteURL === '') { + ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.SITE_URL}); + return; + } else if (global.mm_config.SendEmailNotifications === 'false') { + ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.PREVIEW_MODE}); + return; + } + } + + if (isLicensePastGracePeriod()) { + if (isSystemAdmin) { + ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_EXPIRED, type: BAR_CRITICAL_TYPE}); + } else { + ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_PAST_GRACE, type: BAR_CRITICAL_TYPE}); + } + } 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, type: BAR_CRITICAL_TYPE}); + } + } + + 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; + } + + if (!s.message) { + return false; + } + + if (s.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) { + return false; + } + + return true; + } + + 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); + } + + componentDidUpdate(prevProps, prevState) { + if (!this.props.isLoggedIn) { + return; + } + + 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]}); + } + + handleClose(e) { + if (e) { + 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(this.getState()); + } + + render() { + if (!this.isValidState(this.state)) { + return
; + } + + if (!this.props.isLoggedIn && this.state.type === BAR_ANNOUNCEMENT_TYPE) { + return
; + } + + 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 = ( + + {'×'} + + ); + } + + const renewalLink = RENEWAL_LINK + '?id=' + global.window.mm_license.Id + '&user_count=' + this.state.totalUsers; + + let message = this.state.message; + if (message === ErrorBarTypes.PREVIEW_MODE) { + message = ( + + ); + } else if (message === ErrorBarTypes.LICENSE_EXPIRING) { + message = ( + + ); + } else if (message === ErrorBarTypes.LICENSE_EXPIRED) { + message = ( + + ); + } else if (message === ErrorBarTypes.LICENSE_PAST_GRACE) { + message = ( + + ); + } else if (message === ErrorBarTypes.SITE_URL) { + let id; + let defaultMessage; + if (global.mm_config.EnableSignUpWithGitLab === 'true') { + id = 'error_bar.site_url_gitlab'; + defaultMessage = 'Please configure your {docsLink} in the System Console or in gitlab.rb if you\'re using GitLab Mattermost.'; + } else { + id = 'error_bar.site_url'; + defaultMessage = 'Please configure your {docsLink} in the System Console.'; + } + + message = ( + + + + ), + link: ( + + + + ) + }} + /> + ); + } + + return ( +
+ {message} + {closeButton} +
+ ); + } +} 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 (
- +
- +
diff --git a/webapp/components/error_bar.jsx b/webapp/components/error_bar.jsx deleted file mode 100644 index 97fbbdca0..000000000 --- a/webapp/components/error_bar.jsx +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {Link} from 'react-router'; - -import AnalyticsStore from 'stores/analytics_store.jsx'; -import ErrorStore from 'stores/error_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import {ErrorBarTypes, StatTypes} from 'utils/constants.jsx'; -import {isLicenseExpiring, isLicenseExpired, isLicensePastGracePeriod, displayExpiryDate} from 'utils/license_utils.jsx'; -import * as Utils from 'utils/utils.jsx'; - -const RENEWAL_LINK = 'https://licensing.mattermost.com/renew'; - -const BAR_DEVELOPER_TYPE = 'developer'; -const BAR_CRITICAL_TYPE = 'critical'; - -export default class ErrorBar extends React.Component { - constructor() { - super(); - - this.onErrorChange = this.onErrorChange.bind(this); - this.onAnalyticsChange = this.onAnalyticsChange.bind(this); - this.handleClose = this.handleClose.bind(this); - - ErrorStore.clearLastError(); - - this.setInitialError(); - - this.state = ErrorStore.getLastError() || {}; - } - - setInitialError() { - let isSystemAdmin = false; - const user = UserStore.getCurrentUser(); - if (user) { - isSystemAdmin = Utils.isSystemAdmin(user.roles); - } - - const errorIgnored = ErrorStore.getIgnoreNotification(); - - if (!errorIgnored) { - if (isSystemAdmin && global.mm_config.SiteURL === '') { - ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.SITE_URL}); - return; - } else if (global.mm_config.SendEmailNotifications === 'false') { - ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.PREVIEW_MODE}); - return; - } - } - - if (isLicensePastGracePeriod()) { - if (isSystemAdmin) { - ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_EXPIRED, type: BAR_CRITICAL_TYPE}); - } else { - ErrorStore.storeLastError({notification: true, message: ErrorBarTypes.LICENSE_PAST_GRACE, type: BAR_CRITICAL_TYPE}); - } - } 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}); - } - } - - isValidError(s) { - if (!s) { - return false; - } - - if (!s.message) { - return false; - } - - if (s.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) { - return false; - } - - return true; - } - - componentDidMount() { - ErrorStore.addChangeListener(this.onErrorChange); - AnalyticsStore.addChangeListener(this.onAnalyticsChange); - } - - componentWillUnmount() { - ErrorStore.removeChangeListener(this.onErrorChange); - AnalyticsStore.removeChangeListener(this.onAnalyticsChange); - } - - onErrorChange() { - var newState = ErrorStore.getLastError(); - - if (newState) { - if (newState.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) { - AsyncClient.getStandardAnalytics(); - } - this.setState(newState); - } else { - this.setState({message: null}); - } - } - - onAnalyticsChange() { - const stats = AnalyticsStore.getAllSystem(); - this.setState({totalUsers: stats[StatTypes.TOTAL_USERS]}); - } - - handleClose(e) { - if (e) { - e.preventDefault(); - } - - if (ErrorStore.getLastError() && ErrorStore.getLastError().notification) { - ErrorStore.clearNotificationError(); - } else { - ErrorStore.clearLastError(); - } - - this.setState({message: null}); - } - - render() { - if (!this.isValidError(this.state)) { - return
; - } - - var errClass = 'error-bar'; - - if (this.state.type === BAR_DEVELOPER_TYPE) { - errClass = 'error-bar-developer'; - } else if (this.state.type === BAR_CRITICAL_TYPE) { - errClass = 'error-bar-critical'; - } - - const renewalLink = RENEWAL_LINK + '?id=' + global.window.mm_license.Id + '&user_count=' + this.state.totalUsers; - - let message = this.state.message; - if (message === ErrorBarTypes.PREVIEW_MODE) { - message = ( - - ); - } else if (message === ErrorBarTypes.LICENSE_EXPIRING) { - message = ( - - ); - } else if (message === ErrorBarTypes.LICENSE_EXPIRED) { - message = ( - - ); - } else if (message === ErrorBarTypes.LICENSE_PAST_GRACE) { - message = ( - - ); - } else if (message === ErrorBarTypes.SITE_URL) { - let id; - let defaultMessage; - if (global.mm_config.EnableSignUpWithGitLab === 'true') { - id = 'error_bar.site_url_gitlab'; - defaultMessage = 'Please configure your {docsLink} in the System Console or in gitlab.rb if you\'re using GitLab Mattermost.'; - } else { - id = 'error_bar.site_url'; - defaultMessage = 'Please configure your {docsLink} in the System Console.'; - } - - message = ( - - - - ), - link: ( - - - - ) - }} - /> - ); - } - - return ( -
- {message} - - {'×'} - -
- ); - } -} 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 (
- +
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 (
- +
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 (
- +
{headerButton}
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 (
- +
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index a73068360..e3bc3e027 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -319,6 +319,14 @@ "admin.general.localization.serverLocaleTitle": "Default Server Language:", "admin.general.log": "Logging", "admin.general.policy": "Policy", + "admin.general.policy.enableBannerTitle": "Enable Announcement Banner:", + "admin.general.policy.enableBannerDesc": "Enable an announcement banner across all teams.", + "admin.general.policy.bannerTextTitle": "Banner Text:", + "admin.general.policy.bannerTextDesc": "Text that will appear in the announcement banner.", + "admin.general.policy.bannerColorTitle": "Banner Color:", + "admin.general.policy.bannerTextColorTitle": "Banner Text Color:", + "admin.general.policy.allowBannerDismissalTitle": "Allow Banner Dismissal:", + "admin.general.policy.allowBannerDismissalDesc": "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.", "admin.general.policy.allowEditPostAlways": "Any time", "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.", "admin.general.policy.allowEditPostNever": "Never", diff --git a/webapp/package.json b/webapp/package.json index 3b2e8792a..dc32b6d2e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -30,6 +30,7 @@ "react": "15.5.4", "react-addons-pure-render-mixin": "15.5.2", "react-bootstrap": "0.31.0", + "react-color": "2.11.7", "react-custom-scrollbars": "4.1.2", "react-dom": "15.5.4", "react-intl": "2.3.0", diff --git a/webapp/root.jsx b/webapp/root.jsx index 6c7643f17..161eff48e 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -25,9 +25,11 @@ import 'katex/dist/katex.min.css'; import store from 'stores/redux_store.jsx'; const dispatch = store.dispatch; const getState = store.getState; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {viewChannel} from 'mattermost-redux/actions/channels'; -import {getClientConfig, getLicenseConfig, setUrl} from 'mattermost-redux/actions/general'; +import {getClientConfig, getLicenseConfig, setUrl, setServerVersion as setServerVersionRedux} from 'mattermost-redux/actions/general'; +import {General} from 'mattermost-redux/constants'; // Import the root of our routing tree import rRoot from 'routes/route_root.jsx'; @@ -128,15 +130,19 @@ function renderRootComponent() { let serverVersion = ''; -store.subscribe(() => { - const newServerVersion = getState().entities.general.serverVersion; +EventEmitter.on(General.CONFIG_CHANGED, setServerVersion); + +function setServerVersion(newServerVersion) { if (serverVersion && serverVersion !== newServerVersion) { console.log('Detected version update refreshing the page'); //eslint-disable-line no-console window.location.reload(true); } - serverVersion = newServerVersion; -}); + if (serverVersion !== newServerVersion) { + serverVersion = newServerVersion; + setServerVersionRedux(newServerVersion)(dispatch, getState); + } +} global.window.setup_root = () => { // Do the pre-render setup and call renderRootComponent when done diff --git a/webapp/sass/base/_structure.scss b/webapp/sass/base/_structure.scss index 60673f9a2..df7d75329 100644 --- a/webapp/sass/base/_structure.scss +++ b/webapp/sass/base/_structure.scss @@ -10,6 +10,10 @@ body { height: 100%; position: relative; width: 100%; + + &.error-bar--fixed { + padding-top: 22px; + } } .sticky { diff --git a/webapp/sass/components/_error-bar.scss b/webapp/sass/components/_error-bar.scss index c2ad2402a..765b61ba8 100644 --- a/webapp/sass/components/_error-bar.scss +++ b/webapp/sass/components/_error-bar.scss @@ -4,16 +4,33 @@ background-color: darken($primary-color, 5%); color: $white; padding: 5px 30px; - position: absolute; + position: fixed; text-align: center; top: 0; width: 100%; z-index: 9999; + &.error-bar--fixed { + overflow: hidden; + padding: 1px 30px; + text-overflow: ellipsis; + white-space: nowrap; + } + a { - color: $white !important; + color: $white; text-decoration: underline; + .app__body & { + color: $white; + + &:hover, + &:active, + &:focus { + color: $white; + } + } + &.error-bar__close { color: $white; font-family: 'Open Sans', sans-serif; diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss index d08a9ef45..abe49b9c6 100644 --- a/webapp/sass/layout/_sidebar-left.scss +++ b/webapp/sass/layout/_sidebar-left.scss @@ -8,6 +8,10 @@ width: 220px; z-index: 5; + .error-bar--fixed & { + height: calc(100% - 22px); + } + &.sidebar--padded { padding-top: 44px; } diff --git a/webapp/sass/layout/_sidebar-right.scss b/webapp/sass/layout/_sidebar-right.scss index a1698b3a1..ab73e075b 100644 --- a/webapp/sass/layout/_sidebar-right.scss +++ b/webapp/sass/layout/_sidebar-right.scss @@ -10,6 +10,10 @@ width: 400px; z-index: 8; + .error-bar--fixed & { + height: calc(100% - 22px); + } + &.webrtc { z-index: 5; } @@ -85,8 +89,8 @@ @include opacity(.55); position: absolute; top: -25px; - } - } + } + } } .help__format-text { diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index aece76b28..26e93ec3d 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -12,6 +12,13 @@ overflow: auto; padding: 0 20px; + .color-picker__popover { + position: absolute; + right: 0; + top: 40px; + z-index: 5; + } + .dropdown-menu { .divider { @include opacity(1); diff --git a/webapp/tests/components/admin_console/__snapshots__/color_setting.test.jsx.snap b/webapp/tests/components/admin_console/__snapshots__/color_setting.test.jsx.snap new file mode 100644 index 000000000..7b8c934ce --- /dev/null +++ b/webapp/tests/components/admin_console/__snapshots__/color_setting.test.jsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ColorSetting should match snapshot, all 1`] = ` + +
+ + + + +
+
+`; + +exports[`components/ColorSetting should match snapshot, disabled 1`] = ` + +
+ + + + +
+
+`; + +exports[`components/ColorSetting should match snapshot, no help text 1`] = ` + +
+ + + + +
+
+`; diff --git a/webapp/tests/components/admin_console/color_setting.test.jsx b/webapp/tests/components/admin_console/color_setting.test.jsx new file mode 100644 index 000000000..a1c44a037 --- /dev/null +++ b/webapp/tests/components/admin_console/color_setting.test.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import ColorSetting from 'components/admin_console/color_setting.jsx'; + +describe('components/ColorSetting', () => { + test('should match snapshot, all', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, no help text', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, disabled', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index e4aa48a59..469f793c8 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -1348,3 +1348,16 @@ export function isEmptyObject(object) { export function updateWindowDimensions(component) { component.setState({width: window.innerWidth, height: window.innerHeight}); } + +export function removePrefixFromLocalStorage(prefix) { + const keys = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith(prefix)) { + keys.push(localStorage.key(i)); + } + } + + for (let i = 0; i < keys.length; i++) { + localStorage.removeItem(keys[i]); + } +} diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 703622450..8660bed5e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4792,7 +4792,7 @@ lodash.unset@^4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed" -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.8.0: +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.8.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4874,13 +4874,17 @@ match-at@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.0.tgz#f561e7709ff9a105b85cc62c6b8ee7c15bf24f31" +material-colors@^1.2.1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.5.tgz#5292593e6754cb1bcc2b98030e4e0d6a3afc9ea1" + math-expression-evaluator@^1.2.14: version "1.2.16" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9" mattermost-redux@mattermost/mattermost-redux#webapp-master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/d1652dc7b636aae658d0d109919b6a74762a186d" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/dfa584b61ed9d04c167be53fe16ac6432892ece1" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" @@ -6044,6 +6048,16 @@ react-bootstrap@0.31.0: uncontrollable "^4.1.0" warning "^3.0.0" +react-color@^2.11.7: + version "2.11.7" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.11.7.tgz#746465b75feda63c2567607dfbcb276fc954a5b7" + dependencies: + lodash "^4.0.1" + material-colors "^1.2.1" + prop-types "^15.5.4" + reactcss "^1.2.0" + tinycolor2 "^1.1.2" + react-custom-scrollbars@4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.1.2.tgz#0e60c4a46c4a61f9e4994a7663e2b9cbbc5187a3" @@ -6137,6 +6151,12 @@ react@15.5.4: object-assign "^4.1.0" prop-types "^15.5.7" +reactcss@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.2.tgz#41b0ef43e01d54880357c34b11ac1531209350ef" + dependencies: + lodash "^4.0.1" + read-all-stream@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" @@ -6280,7 +6300,7 @@ redux-persist-transform-filter@0.0.9: lodash.set "^4.3.2" lodash.unset "^4.5.2" -redux-persist@4.6.0, redux-persist@^4.5.0: +redux-persist@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-4.6.0.tgz#3994793d5f2f38bf02591c9e693e16bf8eae2728" dependencies: @@ -6288,7 +6308,7 @@ redux-persist@4.6.0, redux-persist@^4.5.0: lodash "^4.17.4" lodash-es "^4.17.4" -redux-persist@4.8.0: +redux-persist@4.8.0, redux-persist@^4.5.0: version "4.8.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-4.8.0.tgz#17fd998949bdeef9275e4cf60ad5bbe1c73675fc" dependencies: @@ -7257,6 +7277,10 @@ timers-browserify@^2.0.2: dependencies: setimmediate "^1.0.4" +tinycolor2@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + tmp@^0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" -- cgit v1.2.3-1-g7c22