From 49ab8b216191749bd39694d79f687a84ad24adf0 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 11 Apr 2016 13:45:03 -0400 Subject: Add custom branding functionality (#2667) --- webapp/components/admin_console/team_settings.jsx | 297 +++++++++++++++++++++- webapp/components/login/login.jsx | 66 +++-- webapp/i18n/en.json | 11 + webapp/sass/components/_inputs.scss | 4 + webapp/sass/responsive/_mobile.scss | 13 +- webapp/sass/responsive/_tablet.scss | 11 + webapp/sass/routes/_admin-console.scss | 5 + webapp/sass/routes/_signup.scss | 21 ++ webapp/stores/admin_store.jsx | 20 -- webapp/utils/async_client.jsx | 1 - webapp/utils/client.jsx | 19 ++ 11 files changed, 409 insertions(+), 59 deletions(-) (limited to 'webapp') diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx index 654f0085d..d361c989f 100644 --- a/webapp/components/admin_console/team_settings.jsx +++ b/webapp/components/admin_console/team_settings.jsx @@ -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 =
; + } + + let uploadImage; + let uploadText; + if (this.state.enableCustomBrand) { + let img; + if (this.state.brandImage) { + img = ( + + ); + } else if (this.state.brandImageExists) { + img = ( + + ); + } else { + img = ( +

+ +

+ ); + } + + uploadImage = ( +
+ +
+ {img} +
+
+
+
+ + +
+ +
+ {serverImageError} +

+ +

+
+
+ ); + + uploadText = ( +
+ +
+ +

+ +

+
+
+ ); + } + + return ( +
+
+ +
+ + +

+ +

+
+
+ + {uploadImage} + {uploadText} +
+ ); + } + 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 (
@@ -387,6 +654,8 @@ class TeamSettings extends React.Component {
+ {brand} +
{serverError} @@ -417,4 +686,4 @@ TeamSettings.propTypes = { config: React.PropTypes.object }; -export default injectIntl(TeamSettings); \ No newline at end of file +export default injectIntl(TeamSettings); diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login.jsx index ed7495b13..a3dadbf36 100644 --- a/webapp/components/login/login.jsx +++ b/webapp/components/login/login.jsx @@ -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 ( +
+ +

+

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

{currentTeam.display_name}

-

- -

- {content} +
+
+ {customContent} +
+
+
+ +
+

{currentTeam.display_name}

+

+ +

+ {content} +
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 023584e1d..df6a09779 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -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 docs.mattermost.com/administration/config-settings.html#custom-branding.", + "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", diff --git a/webapp/sass/components/_inputs.scss b/webapp/sass/components/_inputs.scss index 42ab56128..c34d0d2d4 100644 --- a/webapp/sass/components/_inputs.scss +++ b/webapp/sass/components/_inputs.scss @@ -33,3 +33,7 @@ fieldset { } } } + +.admin-textarea { + resize: none; +} diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index e3fac21f7..21c3135c2 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -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); } } diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index db2a8d7b9..cb5216dea 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -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%); diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index faa66e08b..6987b59ae 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -344,3 +344,8 @@ } } } + +.brand-img { + margin-bottom: 1.5em; + max-width: 150px; +} diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss index 6d6092170..77ccdf4ed 100644 --- a/webapp/sass/routes/_signup.scss +++ b/webapp/sass/routes/_signup.scss @@ -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; } diff --git a/webapp/stores/admin_store.jsx b/webapp/stores/admin_store.jsx index 0f19dd484..ecfbaf85f 100644 --- a/webapp/stores/admin_store.jsx +++ b/webapp/stores/admin_store.jsx @@ -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() { diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 5b0c221ae..80a08dc21 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1334,4 +1334,3 @@ export function regenCommandToken(id) { } ); } - diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx index 6c784c11c..687d47da4 100644 --- a/webapp/utils/client.jsx +++ b/webapp/utils/client.jsx @@ -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); + } + }); +} -- cgit v1.2.3-1-g7c22