diff --git a/Makefile b/Makefile
index 368b3234b5..131cef5710 100644
--- a/Makefile
+++ b/Makefile
@@ -180,12 +180,15 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/ldap && ./ldap.test -test.v -test.timeout=120s -test.coverprofile=cldap.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/compliance && ./compliance.test -test.v -test.timeout=120s -test.coverprofile=ccompliance.out || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/emoji && ./emoji.test -test.v -test.timeout=120s -test.coverprofile=cemoji.out || exit 1
tail -n +2 cldap.out >> ecover.out
tail -n +2 ccompliance.out >> ecover.out
- rm -f cldap.out ccompliance.out
+ tail -n +2 cemoji.out >> ecover.out
+ rm -f cldap.out ccompliance.out cemoji.out
rm -r ldap.test
rm -r compliance.test
+ rm -r emoji.test
endif
internal-test-web-client: start-docker prepare-enterprise
diff --git a/api/emoji.go b/api/emoji.go
index 24989924a5..d849962304 100644
--- a/api/emoji.go
+++ b/api/emoji.go
@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -32,7 +33,7 @@ func InitEmoji() {
BaseRoutes.Emoji.Handle("/list", ApiUserRequired(getEmoji)).Methods("GET")
BaseRoutes.Emoji.Handle("/create", ApiUserRequired(createEmoji)).Methods("POST")
BaseRoutes.Emoji.Handle("/delete", ApiUserRequired(deleteEmoji)).Methods("POST")
- BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequired(getEmojiImage)).Methods("GET")
+ BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequiredTrustRequester(getEmojiImage)).Methods("GET")
}
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -58,7 +59,8 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !(*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation == model.RESTRICT_EMOJI_CREATION_ALL || c.IsSystemAdmin()) {
+ if emojiInterface := einterfaces.GetEmojiInterface(); emojiInterface != nil &&
+ !emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized
return
diff --git a/api/emoji_test.go b/api/emoji_test.go
index 26dbe9323e..fb23cc4396 100644
--- a/api/emoji_test.go
+++ b/api/emoji_test.go
@@ -22,6 +22,12 @@ func TestGetEmoji(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
+ EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
+ defer func() {
+ *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
+ }()
+ *utils.Cfg.ServiceSettings.EnableCustomEmoji = true
+
emojis := []*model.Emoji{
{
CreatorId: model.NewId(),
@@ -95,13 +101,10 @@ func TestCreateEmoji(t *testing.T) {
Client := th.BasicClient
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
- RestrictCustomEmojiCreation := *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation
defer func() {
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
- *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = RestrictCustomEmojiCreation
}()
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false
- *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
@@ -213,28 +216,6 @@ func TestCreateEmoji(t *testing.T) {
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji as another user")
}
-
- *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ADMIN
-
- // try to create an emoji when only system admins are allowed to create them
- emoji = &model.Emoji{
- CreatorId: th.BasicUser.Id,
- Name: model.NewId(),
- }
- if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
- t.Fatal("shouldn't be able to create an emoji when not a system admin")
- }
-
- emoji = &model.Emoji{
- CreatorId: th.SystemAdminUser.Id,
- Name: model.NewId(),
- }
- if emojiResult, err := th.SystemAdminClient.CreateEmoji(emoji, createTestPng(t, 10, 10), "image.png"); err != nil {
- t.Fatal(err)
- } else {
- emoji = emojiResult
- }
- th.SystemAdminClient.MustGeneric(th.SystemAdminClient.DeleteEmoji(emoji.Id))
}
func TestDeleteEmoji(t *testing.T) {
diff --git a/config/config.json b/config/config.json
index eeb75d0c14..fb325248d3 100644
--- a/config/config.json
+++ b/config/config.json
@@ -5,9 +5,9 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": false,
- "EnableOutgoingWebhooks": false,
- "EnableCommands": false,
+ "EnableIncomingWebhooks": true,
+ "EnableOutgoingWebhooks": true,
+ "EnableCommands": true,
"EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
@@ -24,7 +24,7 @@
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "regular",
- "EnableCustomEmoji": true,
+ "EnableCustomEmoji": false,
"RestrictCustomEmojiCreation": "all"
},
"TeamSettings": {
diff --git a/einterfaces/emoji.go b/einterfaces/emoji.go
new file mode 100644
index 0000000000..f276f6a32e
--- /dev/null
+++ b/einterfaces/emoji.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package einterfaces
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type EmojiInterface interface {
+ CanUserCreateEmoji(string, []*model.TeamMember) bool
+}
+
+var theEmojiInterface EmojiInterface
+
+func RegisterEmojiInterface(newInterface EmojiInterface) {
+ theEmojiInterface = newInterface
+}
+
+func GetEmojiInterface() EmojiInterface {
+ return theEmojiInterface
+}
diff --git a/i18n/en.json b/i18n/en.json
index 3d433dae99..f76c7f637a 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -581,7 +581,7 @@
},
{
"id": "api.emoji.create.parse.app_error",
- "translation": "Unable to create emoji. Image exceeds maximum file size."
+ "translation": "Unable to create emoji. Could not understand request."
},
{
"id": "api.emoji.create.permissions.app_error",
@@ -589,7 +589,7 @@
},
{
"id": "api.emoji.create.too_large.app_error",
- "translation": "Unable to create emoji. Could not understand request."
+ "translation": "Unable to create emoji. Image must be less than 64 KB in size."
},
{
"id": "api.emoji.delete.permissions.app_error",
@@ -621,7 +621,7 @@
},
{
"id": "api.emoji.upload.large_image.app_error",
- "translation": "Unable to create emoji. Image exceeds maximum dimensions."
+ "translation": "Unable to create emoji. Image must be at most 128 by 128 pixels."
},
{
"id": "api.export.json.app_error",
@@ -2079,6 +2079,10 @@
"id": "ent.compliance.run_started.info",
"translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'"
},
+ {
+ "id": "ent.emoji.licence_disable.app_error",
+ "translation": "Custom emoji restrictions disabled by current license. Please contact your system administrator about upgrading your enterprise license."
+ },
{
"id": "ent.ldap.do_login.bind_admin_user.app_error",
"translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword."
diff --git a/model/config.go b/model/config.go
index e71a58a21a..a8c63b1eb8 100644
--- a/model/config.go
+++ b/model/config.go
@@ -38,8 +38,9 @@ const (
FAKE_SETTING = "********************************"
- RESTRICT_EMOJI_CREATION_ALL = "all"
- RESTRICT_EMOJI_CREATION_ADMIN = "system_admin"
+ RESTRICT_EMOJI_CREATION_ALL = "all"
+ RESTRICT_EMOJI_CREATION_ADMIN = "admin"
+ RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
)
type ServiceSettings struct {
diff --git a/webapp/components/admin_console/custom_emoji_settings.jsx b/webapp/components/admin_console/custom_emoji_settings.jsx
index 332c7b216a..738afa3cd6 100644
--- a/webapp/components/admin_console/custom_emoji_settings.jsx
+++ b/webapp/components/admin_console/custom_emoji_settings.jsx
@@ -27,7 +27,10 @@ export default class CustomEmojiSettings extends AdminSettings {
getConfigFromState(config) {
config.ServiceSettings.EnableCustomEmoji = this.state.enableCustomEmoji;
- config.ServiceSettings.RestrictCustomEmojiCreation = this.state.restrictCustomEmojiCreation;
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ config.ServiceSettings.RestrictCustomEmojiCreation = this.state.restrictCustomEmojiCreation;
+ }
return config;
}
@@ -44,6 +47,35 @@ export default class CustomEmojiSettings extends AdminSettings {
}
renderSettings() {
+ let restrictSetting = null;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ restrictSetting = (
+
+ }
+ helpText={
+
+ }
+ value={this.state.restrictCustomEmojiCreation}
+ onChange={this.handleChange}
+ disabled={!this.state.enableCustomEmoji}
+ />
+ );
+ }
+
return (
-
- }
- helpText={
-
- }
- value={this.state.restrictCustomEmojiCreation}
- onChange={this.handleChange}
- disabled={!this.state.enableCustomEmoji}
- />
+ {restrictSetting}
);
}
diff --git a/webapp/components/backstage/backstage_controller.jsx b/webapp/components/backstage/backstage_controller.jsx
new file mode 100644
index 0000000000..690880071e
--- /dev/null
+++ b/webapp/components/backstage/backstage_controller.jsx
@@ -0,0 +1,71 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import TeamStore from 'stores/team_store.jsx';
+
+import BackstageSidebar from './components/backstage_sidebar.jsx';
+import BackstageNavbar from './components/backstage_navbar.jsx';
+import ErrorBar from 'components/error_bar.jsx';
+
+export default class BackstageController extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node.isRequired,
+ params: React.PropTypes.object.isRequired,
+ user: React.PropTypes.user.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onTeamChange = this.onTeamChange.bind(this);
+
+ this.state = {
+ team: props.params.team ? TeamStore.getByName(props.params.team) : TeamStore.getCurrent()
+ };
+ }
+
+ componentDidMount() {
+ TeamStore.addChangeListener(this.onTeamChange);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.onTeamChange);
+ }
+
+ onTeamChange() {
+ this.state = {
+ team: this.props.params.team ? TeamStore.getByName(this.props.params.team) : TeamStore.getCurrent()
+ };
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ {
+ React.Children.map(this.props.children, (child) => {
+ if (!child) {
+ return child;
+ }
+
+ return React.cloneElement(child, {
+ team: this.state.team,
+ user: this.props.user
+ });
+ })
+ }
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/components/backstage_category.jsx
similarity index 97%
rename from webapp/components/backstage/backstage_category.jsx
rename to webapp/components/backstage/components/backstage_category.jsx
index 1d4b11ca3d..74dcf34761 100644
--- a/webapp/components/backstage/backstage_category.jsx
+++ b/webapp/components/backstage/components/backstage_category.jsx
@@ -59,6 +59,7 @@ export default class BackstageCategory extends React.Component {
to={link}
className='category-title'
activeClassName='category-title--active'
+ onlyActiveOnIndex={true}
>
diff --git a/webapp/components/backstage/backstage_header.jsx b/webapp/components/backstage/components/backstage_header.jsx
similarity index 100%
rename from webapp/components/backstage/backstage_header.jsx
rename to webapp/components/backstage/components/backstage_header.jsx
diff --git a/webapp/components/backstage/components/backstage_list.jsx b/webapp/components/backstage/components/backstage_list.jsx
new file mode 100644
index 0000000000..81b8ec4d9b
--- /dev/null
+++ b/webapp/components/backstage/components/backstage_list.jsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import {Link} from 'react-router';
+import LoadingScreen from 'components/loading_screen.jsx';
+
+export default class BackstageList extends React.Component {
+ static propTypes = {
+ children: React.PropTypes.node,
+ header: React.PropTypes.node.isRequired,
+ addLink: React.PropTypes.string,
+ addText: React.PropTypes.node,
+ emptyText: React.PropTypes.node,
+ loading: React.PropTypes.bool.isRequired,
+ searchPlaceholder: React.PropTypes.string
+ }
+
+ static defaultProps = {
+ searchPlaceholder: Utils.localizeMessage('backstage.search', 'Search')
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.updateFilter = this.updateFilter.bind(this);
+
+ this.state = {
+ filter: ''
+ };
+ }
+
+ updateFilter(e) {
+ this.setState({
+ filter: e.target.value
+ });
+ }
+
+ render() {
+ const filter = this.state.filter.toLowerCase();
+
+ let children;
+ if (this.props.loading) {
+ children = ;
+ } else {
+ children = React.Children.map(this.props.children, (child) => {
+ return React.cloneElement(child, {filter});
+ });
+
+ if (children.length === 0 && this.props.emptyText) {
+ children = (
+
+ {this.props.emptyText}
+
+ );
+ }
+ }
+
+ let addLink = null;
+ if (this.props.addLink && this.props.addText) {
+ addLink = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {this.props.header}
+
+ {addLink}
+
+
+
+ {children}
+
+
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/components/backstage_navbar.jsx
similarity index 56%
rename from webapp/components/backstage/backstage_navbar.jsx
rename to webapp/components/backstage/components/backstage_navbar.jsx
index 26ab44c878..7bccfc9f7f 100644
--- a/webapp/components/backstage/backstage_navbar.jsx
+++ b/webapp/components/backstage/components/backstage_navbar.jsx
@@ -1,52 +1,28 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-
import React from 'react';
-import TeamStore from 'stores/team_store.jsx';
-
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router/es6';
export default class BackstageNavbar extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
-
- this.state = {
- team: TeamStore.getCurrent()
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
};
}
- componentDidMount() {
- TeamStore.addChangeListener(this.handleChange);
- $('body').addClass('backstage');
- }
-
- componentWillUnmount() {
- TeamStore.removeChangeListener(this.handleChange);
- $('body').removeClass('backstage');
- }
-
- handleChange() {
- this.setState({
- team: TeamStore.getCurrent()
- });
- }
-
render() {
- if (!this.state.team) {
+ if (!this.props.team) {
return null;
}
return (
-
+
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/components/backstage_section.jsx
similarity index 100%
rename from webapp/components/backstage/backstage_section.jsx
rename to webapp/components/backstage/components/backstage_section.jsx
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/components/backstage_sidebar.jsx
similarity index 52%
rename from webapp/components/backstage/backstage_sidebar.jsx
rename to webapp/components/backstage/components/backstage_sidebar.jsx
index 4d8d8337de..a17d830b0b 100644
--- a/webapp/components/backstage/backstage_sidebar.jsx
+++ b/webapp/components/backstage/components/backstage_sidebar.jsx
@@ -3,13 +3,51 @@
import React from 'react';
-import * as Utils from 'utils/utils.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
import BackstageCategory from './backstage_category.jsx';
import BackstageSection from './backstage_section.jsx';
import {FormattedMessage} from 'react-intl';
export default class BackstageSidebar extends React.Component {
- render() {
+ static get propTypes() {
+ return {
+ team: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object.isRequired
+ };
+ }
+
+ renderCustomEmoji() {
+ if (window.mm_config.EnableCustomEmoji !== 'true') {
+ return null;
+ }
+
+ return (
+
+ }
+ />
+ );
+ }
+
+ renderIntegrations() {
+ if (window.mm_config.EnableIncomingWebhooks !== 'true' &&
+ window.mm_config.EnableOutgoingWebhooks !== 'true' &&
+ window.mm_config.EnableCommands !== 'true') {
+ return null;
+ }
+
+ if (window.mm_config.RestrictCustomEmojiCreation !== 'all' && !TeamStore.isTeamAdmin(this.props.user.id, this.props.team.id)) {
+ return null;
+ }
+
let incomingWebhooks = null;
if (window.mm_config.EnableIncomingWebhooks === 'true') {
incomingWebhooks = (
@@ -55,24 +93,31 @@ export default class BackstageSidebar extends React.Component {
);
}
+ return (
+
+ }
+ >
+ {incomingWebhooks}
+ {outgoingWebhooks}
+ {commands}
+
+ );
+ }
+
+ render() {
return (
-
- }
- >
- {incomingWebhooks}
- {outgoingWebhooks}
- {commands}
-
+ {this.renderCustomEmoji()}
+ {this.renderIntegrations()}
);
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
deleted file mode 100644
index f6de8bc112..0000000000
--- a/webapp/components/backstage/installed_integrations.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import React from 'react';
-
-import * as Utils from 'utils/utils.jsx';
-
-import {Link} from 'react-router/es6';
-import LoadingScreen from 'components/loading_screen.jsx';
-
-export default class InstalledIntegrations extends React.Component {
- static get propTypes() {
- return {
- children: React.PropTypes.node,
- header: React.PropTypes.node.isRequired,
- addLink: React.PropTypes.string.isRequired,
- addText: React.PropTypes.node.isRequired,
- emptyText: React.PropTypes.node.isRequired,
- loading: React.PropTypes.bool.isRequired
- };
- }
-
- constructor(props) {
- super(props);
-
- this.updateFilter = this.updateFilter.bind(this);
-
- this.state = {
- filter: ''
- };
- }
-
- updateFilter(e) {
- this.setState({
- filter: e.target.value
- });
- }
-
- render() {
- const filter = this.state.filter.toLowerCase();
-
- let children;
-
- if (this.props.loading) {
- children = ;
- } else {
- children = React.Children.map(this.props.children, (child) => {
- return React.cloneElement(child, {filter});
- });
-
- if (children.length === 0) {
- children = (
-
- {this.props.emptyText}
-
- );
- }
- }
-
- return (
-
-
-
-
- {this.props.header}
-
-
-
-
-
-
-
- {children}
-
-
-
- );
- }
-}
diff --git a/webapp/components/emoji/components/add_emoji.jsx b/webapp/components/emoji/components/add_emoji.jsx
new file mode 100644
index 0000000000..46f3454766
--- /dev/null
+++ b/webapp/components/emoji/components/add_emoji.jsx
@@ -0,0 +1,307 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import EmojiStore from 'stores/emoji_store.jsx';
+
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {Link} from 'react-router';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class AddEmoji extends React.Component {
+ static propTypes = {
+ team: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object.isRequired
+ }
+
+ static contextTypes = {
+ router: React.PropTypes.object.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateImage = this.updateImage.bind(this);
+
+ this.state = {
+ name: '',
+ image: null,
+ imageUrl: '',
+ saving: false,
+ error: null
+ };
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ error: null
+ });
+
+ const emoji = {
+ creator_id: this.props.user.id,
+ name: this.state.name.trim().toLowerCase()
+ };
+
+ if (!emoji.name) {
+ this.setState({
+ saving: false,
+ error: (
+
+ )
+ });
+
+ return;
+ } else if (/[^a-z0-9_-]/.test(emoji.name)) {
+ this.setState({
+ saving: false,
+ error: (
+
+ )
+ });
+
+ return;
+ } else if (EmojiStore.getSystemEmojis().has(emoji.name)) {
+ this.setState({
+ saving: false,
+ error: (
+
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.image) {
+ this.setState({
+ saving: false,
+ error: (
+
+ )
+ });
+
+ return;
+ }
+
+ AsyncClient.addEmoji(
+ emoji,
+ this.state.image,
+ () => {
+ // for some reason, browserHistory.push doesn't trigger a state change even though the url changes
+ this.context.router.push('/' + this.props.team.name + '/emoji');
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ error: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateImage(e) {
+ if (e.target.files.length === 0) {
+ this.setState({
+ image: null,
+ imageUrl: ''
+ });
+
+ return;
+ }
+
+ const image = e.target.files[0];
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ this.setState({
+ image,
+ imageUrl: reader.result
+ });
+ };
+ reader.readAsDataURL(image);
+ }
+
+ render() {
+ let filename = null;
+ if (this.state.image) {
+ filename = (
+
+ {this.state.image.name}
+
+ );
+ }
+
+ let preview = null;
+ if (this.state.imageUrl) {
+ preview = (
+
+
+
+
+ )
+ }}
+ />
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/webapp/components/emoji/components/emoji_list.jsx b/webapp/components/emoji/components/emoji_list.jsx
new file mode 100644
index 0000000000..5795a57b28
--- /dev/null
+++ b/webapp/components/emoji/components/emoji_list.jsx
@@ -0,0 +1,218 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import EmojiStore from 'stores/emoji_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import EmojiListItem from './emoji_list_item.jsx';
+import {Link} from 'react-router';
+import LoadingScreen from 'components/loading_screen.jsx';
+
+export default class EmojiList extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired,
+ user: React.propTypes.object.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.canCreateEmojis = this.canCreateEmojis.bind(this);
+
+ this.handleEmojiChange = this.handleEmojiChange.bind(this);
+
+ this.deleteEmoji = this.deleteEmoji.bind(this);
+
+ this.updateFilter = this.updateFilter.bind(this);
+
+ this.state = {
+ emojis: EmojiStore.getCustomEmojiMap(),
+ loading: !EmojiStore.hasReceivedCustomEmojis(),
+ filter: ''
+ };
+ }
+
+ componentDidMount() {
+ EmojiStore.addChangeListener(this.handleEmojiChange);
+
+ if (window.mm_config.EnableCustomEmoji === 'true') {
+ AsyncClient.listEmoji();
+ }
+ }
+
+ componentWillUnmount() {
+ EmojiStore.removeChangeListener(this.handleEmojiChange);
+ }
+
+ handleEmojiChange() {
+ this.setState({
+ emojis: EmojiStore.getCustomEmojiMap(),
+ loading: !EmojiStore.hasReceivedCustomEmojis()
+ });
+ }
+
+ updateFilter(e) {
+ this.setState({
+ filter: e.target.value
+ });
+ }
+
+ deleteEmoji(emoji) {
+ AsyncClient.deleteEmoji(emoji.id);
+ }
+
+ canCreateEmojis() {
+ if (global.window.mm_license.IsLicensed !== 'true') {
+ return true;
+ }
+
+ if (Utils.isSystemAdmin(this.props.user.roles)) {
+ return true;
+ }
+
+ if (window.mm_config.RestrictCustomEmojiCreation === 'all') {
+ return true;
+ }
+
+ if (window.mm_config.RestrictCustomEmojiCreation === 'admin') {
+ // check whether the user is an admin on any of their teams
+ for (const member of TeamStore.getTeamMembers()) {
+ if (Utils.isAdmin(member.roles)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ render() {
+ const filter = this.state.filter.toLowerCase();
+ const isSystemAdmin = Utils.isSystemAdmin(this.props.user.roles);
+
+ let emojis = [];
+ if (this.state.loading) {
+ emojis.push(
+
+ );
+ } else if (this.state.emojis.length === 0) {
+ emojis.push(
+
+ |
+
+ |
+
+ );
+ } else {
+ for (const [, emoji] of this.state.emojis) {
+ let onDelete = null;
+ if (isSystemAdmin || this.props.user.id === emoji.creator_id) {
+ onDelete = this.deleteEmoji;
+ }
+
+ emojis.push(
+
+ );
+ }
+ }
+
+ let addLink = null;
+ if (this.canCreateEmojis()) {
+ addLink = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {addLink}
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ {emojis}
+
+
+
+ );
+ }
+}
diff --git a/webapp/components/emoji/components/emoji_list_item.jsx b/webapp/components/emoji/components/emoji_list_item.jsx
new file mode 100644
index 0000000000..50a4bacb17
--- /dev/null
+++ b/webapp/components/emoji/components/emoji_list_item.jsx
@@ -0,0 +1,118 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import EmojiStore from 'stores/emoji_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class EmojiListItem extends React.Component {
+ static get propTypes() {
+ return {
+ emoji: React.PropTypes.object.isRequired,
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleDelete = this.handleDelete.bind(this);
+
+ this.state = {
+ creator: UserStore.getProfile(this.props.emoji.creator_id)
+ };
+ }
+
+ handleDelete(e) {
+ e.preventDefault();
+
+ this.props.onDelete(this.props.emoji);
+ }
+
+ matchesFilter(emoji, creator, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ if (emoji.name.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+
+ if (creator) {
+ if (creator.username.toLowerCase().indexOf(filter) !== -1 ||
+ (creator.first_name && creator.first_name.toLowerCase().indexOf(filter)) ||
+ (creator.last_name && creator.last_name.toLowerCase().indexOf(filter)) ||
+ (creator.nickname && creator.nickname.toLowerCase().indexOf(filter))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ render() {
+ const emoji = this.props.emoji;
+ const creator = this.state.creator;
+ const filter = this.props.filter ? this.props.filter.toLowerCase() : '';
+
+ if (!this.matchesFilter(emoji, creator, filter)) {
+ return null;
+ }
+
+ let creatorName;
+ if (creator) {
+ creatorName = Utils.displayUsernameForUser(creator);
+
+ if (creatorName !== creator.username) {
+ creatorName += ' (@' + creator.username + ')';
+ }
+ } else {
+ creatorName = (
+
+ );
+ }
+
+ let deleteButton = null;
+ if (this.props.onDelete) {
+ deleteButton = (
+
+
+
+ );
+ }
+
+ return (
+
+ |
+ {':' + emoji.name + ':'}
+ |
+
+
+ |
+
+ {creatorName}
+ |
+
+ {deleteButton}
+ |
+
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/integrations/components/add_command.jsx
similarity index 97%
rename from webapp/components/backstage/add_command.jsx
rename to webapp/components/integrations/components/add_command.jsx
index 91af0416b4..e72670e476 100644
--- a/webapp/components/backstage/add_command.jsx
+++ b/webapp/components/integrations/components/add_command.jsx
@@ -6,7 +6,7 @@ import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
-import BackstageHeader from './backstage_header.jsx';
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
import {browserHistory, Link} from 'react-router/es6';
@@ -17,6 +17,12 @@ const REQUEST_POST = 'P';
const REQUEST_GET = 'G';
export default class AddCommand extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
@@ -155,7 +161,7 @@ export default class AddCommand extends React.Component {
AsyncClient.addCommand(
command,
() => {
- browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands');
+ browserHistory.push('/' + this.props.team.name + '/integrations/commands');
},
(err) => {
this.setState({
@@ -300,7 +306,7 @@ export default class AddCommand extends React.Component {
return (