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 ( +
+ + + + + + +
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+
+ + +
+ {filename} +
+ +
+
+
+
+ {preview} +
+ + + + + + + +
+
+
+
+ ); + } +} 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 (
- +
-
+