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
Этот коммит содержится в:
Joram Wilander
2017-06-05 12:49:38 -04:00
коммит произвёл GitHub
родитель 0f3bd85b8d
Коммит abd0466a42
28 изменённых файлов: 651 добавлений и 57 удалений

Просмотреть файл

@@ -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": "",

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}

Просмотреть файл

@@ -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,26 +121,38 @@ 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();
if (newState) {
if (newState.message === ErrorBarTypes.LICENSE_EXPIRING && !this.state.totalUsers) {
AsyncClient.getStandardAnalytics();
}
this.setState(newState);
} else {
this.setState({message: null});
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() {
@@ -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>
);
}

16
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);

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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'/>

Просмотреть файл

@@ -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'>

Просмотреть файл

@@ -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}/>

Просмотреть файл

@@ -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>

Просмотреть файл

@@ -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'/>

Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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

Просмотреть файл

@@ -10,6 +10,10 @@ body {
height: 100%;
position: relative;
width: 100%;
&.error-bar--fixed {
padding-top: 22px;
}
}
.sticky {

Просмотреть файл

@@ -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;

Просмотреть файл

@@ -8,6 +8,10 @@
width: 220px;
z-index: 5;
.error-bar--fixed & {
height: calc(100% - 22px);
}
&.sidebar--padded {
padding-top: 44px;
}

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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>
`;

Просмотреть файл

@@ -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();
});
});

Просмотреть файл

@@ -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]);
}
}

Просмотреть файл

@@ -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"