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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0f3bd85b8d
Коммит
abd0466a42
@@ -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}
|
||||
|
||||
119
webapp/components/admin_console/color_setting.jsx
Обычный файл
119
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
Обычный файл
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>
|
||||
`;
|
||||
55
webapp/tests/components/admin_console/color_setting.test.jsx
Обычный файл
55
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();
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Ссылка в новой задаче
Block a user