Add custom branding functionality (#2667)

Этот коммит содержится в:
Joram Wilander
2016-04-11 13:45:03 -04:00
коммит произвёл Christopher Speller
родитель 5b96ad59c5
Коммит 49ab8b2161
24 изменённых файлов: 654 добавлений и 87 удалений

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

@@ -2,11 +2,11 @@
// See License.txt for license information.
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const holders = defineMessages({
siteNameExample: {
@@ -29,42 +29,91 @@ const holders = defineMessages({
import React from 'react';
const ENABLE_BRAND_ACTION = 'enable_brand_action';
const DISABLE_BRAND_ACTION = 'disable_brand_action';
class TeamSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleImageChange = this.handleImageChange.bind(this);
this.handleImageSubmit = this.handleImageSubmit.bind(this);
this.uploading = false;
this.state = {
saveNeeded: false,
brandImageExists: false,
enableCustomBrand: this.props.config.TeamSettings.EnableCustomBrand,
serverError: null
};
}
handleChange() {
var s = {saveNeeded: true, serverError: this.state.serverError};
componentWillMount() {
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
$.get('/api/v1/admin/get_brand_image').done(() => this.setState({brandImageExists: true}));
}
}
componentDidUpdate() {
if (this.refs.image) {
const reader = new FileReader();
const img = this.refs.image;
reader.onload = (e) => {
$(img).attr('src', e.target.result);
};
reader.readAsDataURL(this.state.brandImage);
}
}
handleChange(action) {
var s = {saveNeeded: true};
if (action === ENABLE_BRAND_ACTION) {
s.enableCustomBrand = true;
}
if (action === DISABLE_BRAND_ACTION) {
s.enableCustomBrand = false;
}
this.setState(s);
}
handleImageChange() {
const element = $(this.refs.fileInput);
if (element.prop('files').length > 0) {
this.setState({fileSelected: true, brandImage: element.prop('files')[0]});
}
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.TeamSettings.SiteName = ReactDOM.findDOMNode(this.refs.SiteName).value.trim();
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked;
config.TeamSettings.SiteName = this.refs.SiteName.value.trim();
config.TeamSettings.RestrictCreationToDomains = this.refs.RestrictCreationToDomains.value.trim();
config.TeamSettings.EnableTeamCreation = this.refs.EnableTeamCreation.checked;
config.TeamSettings.EnableUserCreation = this.refs.EnableUserCreation.checked;
config.TeamSettings.RestrictTeamNames = this.refs.RestrictTeamNames.checked;
config.TeamSettings.EnableTeamListing = this.refs.EnableTeamListing.checked;
config.TeamSettings.EnableCustomBrand = this.refs.EnableCustomBrand.checked;
if (this.refs.CustomBrandText) {
config.TeamSettings.CustomBrandText = this.refs.CustomBrandText.value;
}
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
MaxUsersPerTeam = parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
if (!isNaN(parseInt(this.refs.MaxUsersPerTeam.value, 10))) {
MaxUsersPerTeam = parseInt(this.refs.MaxUsersPerTeam.value, 10);
}
config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
this.refs.MaxUsersPerTeam.value = MaxUsersPerTeam;
Client.saveConfig(
config,
@@ -86,6 +135,219 @@ class TeamSettings extends React.Component {
);
}
handleImageSubmit(e) {
e.preventDefault();
if (!this.state.brandImage) {
return;
}
if (this.uploading) {
return;
}
$('#upload-button').button('loading');
this.uploading = true;
Client.uploadBrandImage(this.state.brandImage,
() => {
$('#upload-button').button('complete');
this.setState({brandImageExists: true, brandImage: null});
this.uploading = false;
},
(err) => {
$('#upload-button').button('reset');
this.uploading = false;
this.setState({serverImageError: err.message});
}
);
}
createBrandSettings() {
var btnClass = 'btn';
if (this.state.fileSelected) {
btnClass = 'btn btn-primary';
}
var serverImageError = '';
if (this.state.serverImageError) {
serverImageError = <div className='form-group has-error'><label className='control-label'>{this.state.serverImageError}</label></div>;
}
let uploadImage;
let uploadText;
if (this.state.enableCustomBrand) {
let img;
if (this.state.brandImage) {
img = (
<img
ref='image'
className='brand-img'
src=''
/>
);
} else if (this.state.brandImageExists) {
img = (
<img
className='brand-img'
src='/api/v1/admin/get_brand_image'
/>
);
} else {
img = (
<p>
<FormattedMessage
id='admin.team.noBrandImage'
defaultMessage='No brand image uploaded'
/>
</p>
);
}
uploadImage = (
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='CustomBrandImage'
>
<FormattedMessage
id='admin.team.brandImageTitle'
defaultMessage='Custom Brand Image:'
/>
</label>
<div className='col-sm-8'>
{img}
</div>
<div className='col-sm-4'/>
<div className='col-sm-8'>
<div className='file__upload'>
<button className='btn btn-default'>
<FormattedMessage
id='admin.team.chooseImage'
defaultMessage='Choose New Image'
/>
</button>
<input
ref='fileInput'
type='file'
accept='.jpg,.png,.bmp'
onChange={this.handleImageChange}
/>
</div>
<button
className={btnClass}
disabled={!this.state.fileSelected}
onClick={this.handleImageSubmit}
id='upload-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.team.uploading', 'Uploading..')}
data-complete-text={'<span class=\'glyphicon glyphicon-ok\'></span> ' + Utils.localizeMessage('admin.team.uploaded', 'Uploaded!')}
>
<FormattedMessage
id='admin.team.upload'
defaultMessage='Upload'
/>
</button>
<br/>
{serverImageError}
<p className='help-text no-margin'>
<FormattedHTMLMessage
id='admin.team.uploadDesc'
defaultMessage='Customize your user experience by adding a custom image to your login screen. See examples at <a href="http://docs.mattermost.com/administration/config-settings.html#custom-branding" target="_blank">docs.mattermost.com/administration/config-settings.html#custom-branding</a>.'
/>
</p>
</div>
</div>
);
uploadText = (
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='CustomBrandText'
>
<FormattedMessage
id='admin.team.brandTextTitle'
defaultMessage='Custom Brand Text:'
/>
</label>
<div className='col-sm-8'>
<textarea
type='text'
rows='5'
maxLength='1024'
className='form-control admin-textarea'
id='CustomBrandText'
ref='CustomBrandText'
onChange={this.handleChange}
>
{this.props.config.TeamSettings.CustomBrandText}
</textarea>
<p className='help-text'>
<FormattedMessage
id='admin.team.brandTextDescription'
defaultMessage='The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.'
/>
</p>
</div>
</div>
);
}
return (
<div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableCustomBrand'
>
<FormattedMessage
id='admin.team.brandTitle'
defaultMessage='Enable Custom Branding: '
/>
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableCustomBrand'
value='true'
ref='EnableCustomBrand'
defaultChecked={this.props.config.TeamSettings.EnableCustomBrand}
onChange={this.handleChange.bind(this, ENABLE_BRAND_ACTION)}
/>
<FormattedMessage
id='admin.team.true'
defaultMessage='true'
/>
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableCustomBrand'
value='false'
defaultChecked={!this.props.config.TeamSettings.EnableCustomBrand}
onChange={this.handleChange.bind(this, DISABLE_BRAND_ACTION)}
/>
<FormattedMessage
id='admin.team.false'
defaultMessage='false'
/>
</label>
<p className='help-text'>
<FormattedMessage
id='admin.team.brandDesc'
defaultMessage='Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.'
/>
</p>
</div>
</div>
{uploadImage}
{uploadText}
</div>
);
}
render() {
const {formatMessage} = this.props.intl;
var serverError = '';
@@ -98,6 +360,11 @@ class TeamSettings extends React.Component {
saveClass = 'btn btn-primary';
}
let brand;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
brand = this.createBrandSettings();
}
return (
<div className='wrapper--fixed'>
@@ -387,6 +654,8 @@ class TeamSettings extends React.Component {
</div>
</div>
{brand}
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
@@ -417,4 +686,4 @@ TeamSettings.propTypes = {
config: React.PropTypes.object
};
export default injectIntl(TeamSettings);
export default injectIntl(TeamSettings);

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

@@ -9,6 +9,7 @@ import LoginMfa from './components/login_mfa.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Client from 'utils/client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -134,6 +135,24 @@ export default class Login extends React.Component {
);
}
}
createCustomLogin() {
if (global.window.mm_license.IsLicensed === 'true' &&
global.window.mm_license.CustomBrand === 'true' &&
global.window.mm_config.EnableCustomBrand === 'true') {
const text = global.window.mm_config.CustomBrandText || '';
return (
<div>
<img
src='/api/v1/admin/get_brand_image'
/>
<p dangerouslySetInnerHTML={{__html: TextFormatting.formatText(text)}}/>
</div>
);
}
return null;
}
createLoginOptions(currentTeam) {
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
@@ -364,6 +383,8 @@ export default class Login extends React.Component {
}
let content;
let customContent;
let customClass;
if (this.state.showMfa) {
content = (
<LoginMfa
@@ -375,6 +396,10 @@ export default class Login extends React.Component {
);
} else {
content = this.createLoginOptions(currentTeam);
customContent = this.createCustomLogin();
if (customContent) {
customClass = 'branded';
}
}
return (
@@ -388,24 +413,29 @@ export default class Login extends React.Component {
</Link>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{currentTeam.display_name}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{content}
<div className={'signup-team__container ' + customClass}>
<div className='signup__markdown'>
{customContent}
</div>
<div className='signup__content'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{currentTeam.display_name}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{content}
</div>
</div>
</div>
</div>

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

@@ -488,6 +488,17 @@
"admin.system_analytics.activeUsers": "Active Users With Posts",
"admin.system_analytics.title": "the System",
"admin.system_analytics.totalPosts": "Total Posts",
"admin.team.noBrandImage": "No brand image uploaded",
"admin.team.brandImageTitle": "Custom Brand Image:",
"admin.team.chooseImage": "Choose New Image",
"admin.team.uploading": "Uploading..",
"admin.team.uploaded": "Uploaded!",
"admin.team.upload": "Upload",
"admin.team.uploadDesc": "Customize your user experience by adding a custom image to your login screen. See examples at <a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>.",
"admin.team.brandTextTitle": "Custom Brand Text:",
"admin.team.brandTextDescription": "The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.",
"admin.team.brandTitle": "Enable Custom Branding: ",
"admin.team.brandDesc": "Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.",
"admin.team.dirDesc": "When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.",
"admin.team.dirTitle": "Enable Team Directory: ",
"admin.team.false": "false",

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

@@ -33,3 +33,7 @@ fieldset {
}
}
}
.admin-textarea {
resize: none;
}

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

@@ -738,16 +738,17 @@
.inner-wrap {
@include single-transition(all, .5s, ease);
&:before{
content:"";
&:before {
//Some trickery in order for the z-index transition to happen immediately on move-in and delayed on move-out.
transition: background-color 0.5s ease, z-index 0s ease 0.5s;
background-color: transparent;
content: '';
height: 100%;
width: calc(100% + 30px);
left: -15px;
position: absolute;
top: 0;
transition: background-color 0.5s ease, z-index 0s ease 0.5s;
width: calc(100% + 30px);
z-index: 0;
}
@@ -755,9 +756,9 @@
@include translate3d(290px, 0, 0);
&:before {
background-color: rgba(0, 0, 0, .4);
transition: background-color .5s ease;
z-index: 9999;
transition: background-color 0.5s ease;
background-color: rgba(0, 0, 0, 0.4);
}
}

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

@@ -1,6 +1,17 @@
@charset 'UTF-8';
@media screen and (max-width: 960px) {
.signup-team__container {
&.branded {
display: block;
margin: 0 auto;
max-width: 380px;
.signup__markdown {
display: none;
}
}
}
.sidebar--right {
@include single-transition(all, .5s, ease);
@include translateX(100%);

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

@@ -344,3 +344,8 @@
}
}
}
.brand-img {
margin-bottom: 1.5em;
max-width: 150px;
}

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

@@ -10,12 +10,33 @@
margin-right: 5px;
}
}
.signup-team__container {
margin: 0 auto;
max-width: 380px;
padding: 100px 0 50px;
position: relative;
&.branded {
@include display-flex;
@include flex-direction(row);
max-width: 900px;
.signup__markdown {
@include flex(1.3 0 0);
padding-right: 80px;
p {
color: lighten($black, 50%);
}
}
.signup__content {
@include flex(1 0 0);
}
}
&.padding--less {
padding-top: 50px;
}

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

@@ -24,26 +24,6 @@ class AdminStoreClass extends EventEmitter {
this.config = null;
this.teams = null;
this.complianceReports = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
this.emitAuditChange = this.emitAuditChange.bind(this);
this.addAuditChangeListener = this.addAuditChangeListener.bind(this);
this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this);
this.emitComplianceReportsChange = this.emitComplianceReportsChange.bind(this);
this.addComplianceReportsChangeListener = this.addComplianceReportsChangeListener.bind(this);
this.removeComplianceReportsChangeListener = this.removeComplianceReportsChangeListener.bind(this);
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
}
emitLogChange() {

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

@@ -1334,4 +1334,3 @@ export function regenCommandToken(id) {
}
);
}

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

@@ -1738,3 +1738,22 @@ export function updateMfa(data, success, error) {
}
});
}
export function uploadBrandImage(image, success, error) {
const formData = new FormData();
formData.append('image', image, image.name);
$.ajax({
url: '/api/v1/admin/upload_brand_image',
type: 'POST',
data: formData,
cache: false,
contentType: false,
processData: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('uploadBrandImage', xhr, status, err);
error(e);
}
});
}