summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/config.json7
-rw-r--r--model/config.go34
-rw-r--r--model/license.go6
-rw-r--r--utils/config.go8
-rw-r--r--utils/license.go1
-rw-r--r--webapp/components/admin_console/admin_console.jsx9
-rw-r--r--webapp/components/admin_console/color_setting.jsx119
-rw-r--r--webapp/components/admin_console/policy_settings.jsx92
-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.js16
-rw-r--r--webapp/components/backstage/backstage_controller.jsx4
-rw-r--r--webapp/components/create_team/create_team_controller.jsx4
-rw-r--r--webapp/components/login/login_controller.jsx4
-rw-r--r--webapp/components/needs_team/needs_team.jsx4
-rw-r--r--webapp/components/select_team/select_team.jsx4
-rw-r--r--webapp/components/signup/signup_controller.jsx4
-rwxr-xr-xwebapp/i18n/en.json8
-rw-r--r--webapp/package.json1
-rw-r--r--webapp/root.jsx16
-rw-r--r--webapp/sass/base/_structure.scss4
-rw-r--r--webapp/sass/components/_error-bar.scss21
-rw-r--r--webapp/sass/layout/_sidebar-left.scss4
-rw-r--r--webapp/sass/layout/_sidebar-right.scss8
-rw-r--r--webapp/sass/routes/_admin-console.scss7
-rw-r--r--webapp/tests/components/admin_console/__snapshots__/color_setting.test.jsx.snap95
-rw-r--r--webapp/tests/components/admin_console/color_setting.test.jsx55
-rw-r--r--webapp/utils/utils.jsx13
-rw-r--r--webapp/yarn.lock32
28 files changed, 650 insertions, 56 deletions
diff --git a/config/config.json b/config/config.json
index d8384a9e3..31b1b1a45 100644
--- a/config/config.json
+++ b/config/config.json
@@ -171,6 +171,13 @@
"ReportAProblemLink": "https://about.mattermost.com/default-report-a-problem/",
"SupportEmail": "feedback@mattermost.com"
},
+ "AnnouncementSettings": {
+ "EnableBanner": false,
+ "BannerText": "",
+ "BannerColor": "#f2a93b",
+ "BannerTextColor": "#333333",
+ "AllowBannerDismissal": true
+ },
"GitLabSettings": {
"Enable": false,
"Secret": "",
diff --git a/model/config.go b/model/config.go
index f3ce6f2f1..4e3a3f7cc 100644
--- a/model/config.go
+++ b/model/config.go
@@ -292,6 +292,14 @@ type SupportSettings struct {
SupportEmail *string
}
+type AnnouncementSettings struct {
+ EnableBanner *bool
+ BannerText *string
+ BannerColor *string
+ BannerTextColor *string
+ AllowBannerDismissal *bool
+}
+
type TeamSettings struct {
SiteName string
MaxUsersPerTeam int
@@ -429,6 +437,7 @@ type Config struct {
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
SupportSettings SupportSettings
+ AnnouncementSettings AnnouncementSettings
GitLabSettings SSOSettings
GoogleSettings SSOSettings
Office365Settings SSOSettings
@@ -825,6 +834,31 @@ func (o *Config) SetDefaults() {
*o.SupportSettings.SupportEmail = SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL
}
+ if o.AnnouncementSettings.EnableBanner == nil {
+ o.AnnouncementSettings.EnableBanner = new(bool)
+ *o.AnnouncementSettings.EnableBanner = false
+ }
+
+ if o.AnnouncementSettings.BannerText == nil {
+ o.AnnouncementSettings.BannerText = new(string)
+ *o.AnnouncementSettings.BannerText = ""
+ }
+
+ if o.AnnouncementSettings.BannerColor == nil {
+ o.AnnouncementSettings.BannerColor = new(string)
+ *o.AnnouncementSettings.BannerColor = "#f2a93b"
+ }
+
+ if o.AnnouncementSettings.BannerTextColor == nil {
+ o.AnnouncementSettings.BannerTextColor = new(string)
+ *o.AnnouncementSettings.BannerTextColor = "#333333"
+ }
+
+ if o.AnnouncementSettings.AllowBannerDismissal == nil {
+ o.AnnouncementSettings.AllowBannerDismissal = new(bool)
+ *o.AnnouncementSettings.AllowBannerDismissal = true
+ }
+
if o.LdapSettings.Enable == nil {
o.LdapSettings.Enable = new(bool)
*o.LdapSettings.Enable = false
diff --git a/model/license.go b/model/license.go
index 558cd43fa..443d78282 100644
--- a/model/license.go
+++ b/model/license.go
@@ -50,6 +50,7 @@ type Features struct {
SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"`
ElasticSearch *bool `json:"elastic_search"`
+ Announcement *bool `json:"announcement"`
// after we enabled more features for webrtc we'll need to control them with this
FutureFeatures *bool `json:"future_features"`
}
@@ -142,6 +143,11 @@ func (f *Features) SetDefaults() {
f.ElasticSearch = new(bool)
*f.ElasticSearch = *f.FutureFeatures
}
+
+ if f.Announcement == nil {
+ f.Announcement = new(bool)
+ *f.Announcement = true
+ }
}
func (l *License) IsExpired() bool {
diff --git a/utils/config.go b/utils/config.go
index c0771933d..b5a26d298 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -538,6 +538,14 @@ func getClientConfig(c *model.Config) map[string]string {
props["ElasticSearchEnableIndexing"] = strconv.FormatBool(*c.ElasticSearchSettings.EnableIndexing)
props["ElasticSearchEnableSearching"] = strconv.FormatBool(*c.ElasticSearchSettings.EnableSearching)
}
+
+ if *License.Features.Announcement {
+ props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
+ props["BannerText"] = *c.AnnouncementSettings.BannerText
+ props["BannerColor"] = *c.AnnouncementSettings.BannerColor
+ props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
+ props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
+ }
}
return props
diff --git a/utils/license.go b/utils/license.go
index 03a9d7ab3..3647b51cc 100644
--- a/utils/license.go
+++ b/utils/license.go
@@ -178,6 +178,7 @@ func getClientLicense(l *model.License) map[string]string {
props["CustomBrand"] = strconv.FormatBool(*l.Features.CustomBrand)
props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS)
props["PasswordRequirements"] = strconv.FormatBool(*l.Features.PasswordRequirements)
+ props["Announcement"] = strconv.FormatBool(*l.Features.Announcement)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
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'/>
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`] = `
+<Settings
+ helpText="helptext"
+ inputId="id"
+ label="label"
+>
+ <div
+ className="input-group color-picker colorpicker-element"
+ >
+ <input
+ className="form-control"
+ disabled={false}
+ onChange={[Function]}
+ type="text"
+ value="#fff"
+ />
+ <span
+ className="input-group-addon picker-id"
+ onClick={[Function]}
+ >
+ <i
+ style={
+ Object {
+ "backgroundColor": "#fff",
+ }
+ }
+ />
+ </span>
+ </div>
+</Settings>
+`;
+
+exports[`components/ColorSetting should match snapshot, disabled 1`] = `
+<Settings
+ inputId="id"
+ label="label"
+>
+ <div
+ className="input-group color-picker colorpicker-element"
+ >
+ <input
+ className="form-control"
+ disabled={true}
+ onChange={[Function]}
+ type="text"
+ value="#fff"
+ />
+ <span
+ className="input-group-addon picker-id"
+ onClick={[Function]}
+ >
+ <i
+ style={
+ Object {
+ "backgroundColor": "#fff",
+ }
+ }
+ />
+ </span>
+ </div>
+</Settings>
+`;
+
+exports[`components/ColorSetting should match snapshot, no help text 1`] = `
+<Settings
+ inputId="id"
+ label="label"
+>
+ <div
+ className="input-group color-picker colorpicker-element"
+ >
+ <input
+ className="form-control"
+ disabled={false}
+ onChange={[Function]}
+ type="text"
+ value="#fff"
+ />
+ <span
+ className="input-group-addon picker-id"
+ onClick={[Function]}
+ >
+ <i
+ style={
+ Object {
+ "backgroundColor": "#fff",
+ }
+ }
+ />
+ </span>
+ </div>
+</Settings>
+`;
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(
+ <ColorSetting
+ id='id'
+ label='label'
+ helpText='helptext'
+ value='#fff'
+ onChange={emptyFunction}
+ disabled={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, no help text', () => {
+ function emptyFunction() {} //eslint-disable-line no-empty-function
+
+ const wrapper = shallow(
+ <ColorSetting
+ id='id'
+ label='label'
+ value='#fff'
+ onChange={emptyFunction}
+ disabled={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, disabled', () => {
+ function emptyFunction() {} //eslint-disable-line no-empty-function
+
+ const wrapper = shallow(
+ <ColorSetting
+ id='id'
+ label='label'
+ value='#fff'
+ onChange={emptyFunction}
+ disabled={true}
+ />
+ );
+ 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"