diff --git a/api/channel.go b/api/channel.go index 151627623f..de15b3610b 100644 --- a/api/channel.go +++ b/api/channel.go @@ -397,7 +397,7 @@ func JoinChannel(c *Context, channelId string, role string) { } } -func JoinDefaultChannels(c *Context, user *model.User, channelRole string) *model.AppError { +func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError { // We don't call JoinChannel here since c.Session is not populated on user creation var err *model.AppError = nil diff --git a/api/import.go b/api/import.go new file mode 100644 index 0000000000..e3f314d203 --- /dev/null +++ b/api/import.go @@ -0,0 +1,57 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +// +// Import functions are sutible for entering posts and users into the database without +// some of the usual checks. (IsValid is still run) +// + +func ImportPost(post *model.Post) { + post.Hashtags, _ = model.ParseHashtags(post.Message) + + if result := <-Srv.Store.Post().Save(post); result.Err != nil { + l4g.Debug("Error saving post. user=" + post.UserId + ", message=" + post.Message) + } +} + +func ImportUser(user *model.User) *model.User { + user.MakeNonNil() + if len(user.Props["theme"]) == 0 { + user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor) + } + + if result := <-Srv.Store.User().Save(user); result.Err != nil { + l4g.Error("Error saving user. err=%v", result.Err) + return nil + } else { + ruser := result.Data.(*model.User) + + if err := JoinDefaultChannels(ruser, ""); err != nil { + l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) + } + + if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { + l4g.Error("Failed to set email verified err=%v", cresult.Err) + } + + return ruser + } +} + +func ImportChannel(channel *model.Channel) *model.Channel { + if result := <-Srv.Store.Channel().Save(channel); result.Err != nil { + return nil + } else { + sc := result.Data.(*model.Channel) + + return sc + } +} diff --git a/api/slackimport.go b/api/slackimport.go new file mode 100644 index 0000000000..ca3fdf3d18 --- /dev/null +++ b/api/slackimport.go @@ -0,0 +1,244 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "archive/zip" + "bytes" + l4g "code.google.com/p/log4go" + "encoding/json" + "github.com/mattermost/platform/model" + "io" + "mime/multipart" + "strconv" + "strings" +) + +type SlackChannel struct { + Id string `json:"id"` + Name string `json:"name"` + Members []string `json:"members"` + Topic map[string]string `json:"topic"` +} + +type SlackUser struct { + Id string `json:"id"` + Username string `json:"name"` + Profile map[string]string `json:"profile"` +} + +type SlackPost struct { + User string `json:"user"` + BotId string `json:"bot_id"` + BotUsername string `json:"username"` + Text string `json:"text"` + TimeStamp string `json:"ts"` + Type string `json:"type"` + SubType string `json:"subtype"` + Comment map[string]string `json:"comment"` +} + +func SlackConvertTimeStamp(ts string) int64 { + timeString := strings.SplitN(ts, ".", 2)[0] + + timeStamp, err := strconv.ParseInt(timeString, 10, 64) + if err != nil { + l4g.Warn("Bad timestamp detected") + return 1 + } + return timeStamp * 1000 // Convert to milliseconds +} + +func SlackParseChannels(data io.Reader) []SlackChannel { + decoder := json.NewDecoder(data) + + var channels []SlackChannel + if err := decoder.Decode(&channels); err != nil { + return make([]SlackChannel, 0) + } + return channels +} + +func SlackParseUsers(data io.Reader) []SlackUser { + decoder := json.NewDecoder(data) + + var users []SlackUser + if err := decoder.Decode(&users); err != nil { + return make([]SlackUser, 0) + } + return users +} + +func SlackParsePosts(data io.Reader) []SlackPost { + decoder := json.NewDecoder(data) + + var posts []SlackPost + if err := decoder.Decode(&posts); err != nil { + return make([]SlackPost, 0) + } + return posts +} + +func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map[string]*model.User { + // Log header + log.WriteString("\n Users Created\n") + log.WriteString("===============\n\n") + + addedUsers := make(map[string]*model.User) + for _, sUser := range slackusers { + firstName := "" + lastName := "" + if name, ok := sUser.Profile["first_name"]; ok { + firstName = name + } + if name, ok := sUser.Profile["last_name"]; ok { + lastName = name + } + + password := model.NewId() + + newUser := model.User{ + TeamId: teamId, + Username: sUser.Username, + FirstName: firstName, + LastName: lastName, + Email: sUser.Profile["email"], + Password: password, + } + + if mUser := ImportUser(&newUser); mUser != nil { + addedUsers[sUser.Id] = mUser + log.WriteString("Email, Password: " + newUser.Email + ", " + password + "\n") + } else { + log.WriteString("Unable to import user: " + sUser.Username) + } + } + + return addedUsers +} + +func SlackAddPosts(channel *model.Channel, posts []SlackPost, users map[string]*model.User) { + for _, sPost := range posts { + switch { + case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"): + if sPost.User == "" { + l4g.Debug("Message without user") + continue + } else if users[sPost.User] == nil { + l4g.Debug("User: " + sPost.User + " does not exist!") + continue + } + newPost := model.Post{ + UserId: users[sPost.User].Id, + ChannelId: channel.Id, + Message: sPost.Text, + CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), + } + ImportPost(&newPost) + case sPost.Type == "message" && sPost.SubType == "file_comment": + if sPost.Comment["user"] == "" { + l4g.Debug("Message without user") + continue + } else if users[sPost.Comment["user"]] == nil { + l4g.Debug("User: " + sPost.User + " does not exist!") + continue + } + newPost := model.Post{ + UserId: users[sPost.Comment["user"]].Id, + ChannelId: channel.Id, + Message: sPost.Comment["comment"], + CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), + } + ImportPost(&newPost) + case sPost.Type == "message" && sPost.SubType == "bot_message": + // In the future this will use the "Action Post" spec to post + // a message without using a username. For now we just warn that we don't handle this case + l4g.Warn("Slack bot posts are not imported yet") + default: + l4g.Warn("Unsupported post type: " + sPost.Type + ", " + sPost.SubType) + } + } +} + +func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[string][]SlackPost, users map[string]*model.User, log *bytes.Buffer) map[string]*model.Channel { + // Write Header + log.WriteString("\n Channels Added \n") + log.WriteString("=================\n\n") + + addedChannels := make(map[string]*model.Channel) + for _, sChannel := range slackchannels { + newChannel := model.Channel{ + TeamId: teamId, + Type: model.CHANNEL_OPEN, + DisplayName: sChannel.Name, + Name: sChannel.Name, + Description: sChannel.Topic["value"], + } + mChannel := ImportChannel(&newChannel) + if mChannel == nil { + // Maybe it already exists? + if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err != nil { + l4g.Debug("Failed to import: %s", newChannel.DisplayName) + log.WriteString("Failed to import: " + newChannel.DisplayName + "\n") + continue + } else { + mChannel = result.Data.(*model.Channel) + log.WriteString("Merged with existing channel: " + newChannel.DisplayName + "\n") + } + } + log.WriteString(newChannel.DisplayName + "\n") + addedChannels[sChannel.Id] = mChannel + SlackAddPosts(mChannel, posts[sChannel.Name], users) + } + + return addedChannels +} + +func SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) { + zipreader, err := zip.NewReader(fileData, fileSize) + if err != nil || zipreader.File == nil { + return model.NewAppError("SlackImport", "Unable to open zip file", err.Error()), nil + } + + // Create log file + log := bytes.NewBufferString("Mattermost Slack Import Log\n") + + var channels []SlackChannel + var users []SlackUser + posts := make(map[string][]SlackPost) + for _, file := range zipreader.File { + reader, err := file.Open() + if err != nil { + return model.NewAppError("SlackImport", "Unable to open: "+file.Name, err.Error()), log + } + if file.Name == "channels.json" { + channels = SlackParseChannels(reader) + } else if file.Name == "users.json" { + users = SlackParseUsers(reader) + } else { + spl := strings.Split(file.Name, "/") + if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") { + newposts := SlackParsePosts(reader) + channel := spl[0] + if _, ok := posts[channel]; ok == false { + posts[channel] = newposts + } else { + posts[channel] = append(posts[channel], newposts...) + } + } + + } + } + + addedUsers := SlackAddUsers(teamID, users, log) + SlackAddChannels(teamID, channels, posts, addedUsers, log) + + log.WriteString("\n Notes \n") + log.WriteString("=======\n\n") + + log.WriteString("- Some posts may not have been imported because they where not supported by this importer.\n") + log.WriteString("- Slack bot posts are currently not supported.\n") + + return nil, log +} diff --git a/api/team.go b/api/team.go index e6b8f4e142..a331e9e348 100644 --- a/api/team.go +++ b/api/team.go @@ -4,6 +4,7 @@ package api import ( + "bytes" l4g "code.google.com/p/log4go" "fmt" "github.com/gorilla/mux" @@ -13,6 +14,7 @@ import ( "net/url" "strconv" "strings" + "time" ) func InitTeam(r *mux.Router) { @@ -29,6 +31,7 @@ func InitTeam(r *mux.Router) { sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST") sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { @@ -489,3 +492,70 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } } + +func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.HasPermissionsToTeam(c.Session.TeamId, "import") || !c.IsTeamAdmin(c.Session.UserId) { + c.Err = model.NewAppError("importTeam", "Only a team admin can import data.", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + if err := r.ParseMultipartForm(10000000); err != nil { + c.Err = model.NewAppError("importTeam", "Could not parse multipart form", err.Error()) + return + } + + importFromArray, ok := r.MultipartForm.Value["importFrom"] + importFrom := importFromArray[0] + + fileSizeStr, ok := r.MultipartForm.Value["filesize"] + if !ok { + c.Err = model.NewAppError("importTeam", "Filesize unavilable", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + fileSize, err := strconv.ParseInt(fileSizeStr[0], 10, 64) + if err != nil { + c.Err = model.NewAppError("importTeam", "Filesize not an integer", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + fileInfoArray, ok := r.MultipartForm.File["file"] + if !ok { + c.Err = model.NewAppError("importTeam", "No file under 'file' in request", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if len(fileInfoArray) <= 0 { + c.Err = model.NewAppError("importTeam", "Empty array under 'file' in request", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + fileInfo := fileInfoArray[0] + + fileData, err := fileInfo.Open() + defer fileData.Close() + if err != nil { + c.Err = model.NewAppError("importTeam", "Could not open file", err.Error()) + c.Err.StatusCode = http.StatusBadRequest + return + } + + var log *bytes.Buffer + switch importFrom { + case "slack": + var err *model.AppError + if err, log = SlackImport(fileData, fileSize, c.Session.TeamId); err != nil { + c.Err = err + c.Err.StatusCode = http.StatusBadRequest + } + } + + w.Header().Set("Content-Disposition", "attachment; filename=MattermostImportLog.txt") + w.Header().Set("Content-Type", "application/octet-stream") + http.ServeContent(w, r, "MattermostImportLog.txt", time.Now(), bytes.NewReader(log.Bytes())) +} diff --git a/api/user.go b/api/user.go index 2e71ddfc6e..05ccd03e89 100644 --- a/api/user.go +++ b/api/user.go @@ -181,12 +181,13 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { if result := <-Srv.Store.User().Save(user); result.Err != nil { c.Err = result.Err + l4g.Error("Filae err=%v", result.Err) return nil } else { ruser := result.Data.(*model.User) // Soft error if there is an issue joining the default channels - if err := JoinDefaultChannels(c, ruser, channelRole); err != nil { + if err := JoinDefaultChannels(ruser, channelRole); err != nil { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } diff --git a/model/post.go b/model/post.go index 0c035d4e79..f8a3032a39 100644 --- a/model/post.go +++ b/model/post.go @@ -120,7 +120,9 @@ func (o *Post) PreSave() { o.OriginalId = "" - o.CreateAt = GetMillis() + if o.CreateAt <= 0 { + o.CreateAt = GetMillis() + } o.UpdateAt = o.CreateAt if o.Props == nil { diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 49eb58773a..d3d386534f 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -3,7 +3,7 @@ module.exports = React.createClass({ render: function() { - var client_error = this.props.client_error ?
: null; + var clientError = this.props.clientError ?
: null; var server_error = this.props.server_error ?
: null; var inputs = this.props.inputs; @@ -19,7 +19,7 @@ module.exports = React.createClass({

  • { server_error } - { client_error } + { clientError } { this.props.submit ? Submit : "" } Cancel
  • diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx new file mode 100644 index 0000000000..870710850f --- /dev/null +++ b/web/react/components/setting_upload.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + displayName: 'Setting Upload', + propTypes: { + title: React.PropTypes.string.isRequired, + submit: React.PropTypes.func.isRequired, + fileTypesAccepted: React.PropTypes.string.isRequired, + clientError: React.PropTypes.string, + serverError: React.PropTypes.string + }, + getInitialState: function() { + return { + clientError: this.props.clientError, + serverError: this.props.serverError + }; + }, + componentWillReceiveProps: function() { + this.setState({ + clientError: this.props.clientError, + serverError: this.props.serverError + }); + }, + doFileSelect: function(e) { + e.preventDefault(); + this.setState({ + clientError: '', + serverError: '' + }); + }, + doSubmit: function(e) { + e.preventDefault(); + var inputnode = this.refs.uploadinput.getDOMNode(); + if (inputnode.files && inputnode.files[0]) { + this.props.submit(inputnode.files[0]); + } else { + this.setState({clientError: 'No file selected.'}); + } + }, + doCancel: function(e) { + e.preventDefault(); + this.refs.uploadinput.getDOMNode().value = ''; + this.setState({ + clientError: '', + serverError: '' + }); + }, + render: function() { + var clientError = null; + if (this.state.clientError) { + clientError = ( +
    + ); + } + var serverError = null; + if (this.state.serverError) { + serverError = ( +
    + ); + } + return ( + + ); + } +}); diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index b4d291622a..d8091ec28d 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -15,7 +15,7 @@ module.exports = React.createClass({
    diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx new file mode 100644 index 0000000000..ee0bfa8747 --- /dev/null +++ b/web/react/components/team_feature_tab.jsx @@ -0,0 +1,147 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); + +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); + +module.exports = React.createClass({ + displayName: 'Feature Tab', + propTypes: { + updateSection: React.PropTypes.func.isRequired, + team: React.PropTypes.object.isRequired, + activeSection: React.PropTypes.string.isRequired + }, + submitValetFeature: function() { + var data = {}; + data.allowValet = this.state.allowValet; + + client.updateValetFeature(data, + function() { + this.props.updateSection(''); + AsyncClient.getMyTeam(); + }.bind(this), + function(err) { + var state = this.getInitialState(); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + }, + handleValetRadio: function(val) { + this.setState({allowValet: val}); + this.refs.wrapper.getDOMNode().focus(); + }, + componentWillReceiveProps: function(newProps) { + var team = newProps.team; + + var allowValet = 'false'; + if (team && team.allowValet) { + allowValet = 'true'; + } + + this.setState({allowValet: allowValet}); + }, + getInitialState: function() { + var team = this.props.team; + + var allowValet = 'false'; + if (team && team.allowValet) { + allowValet = 'true'; + } + + return {allowValet: allowValet}; + }, + onUpdateSection: function() { + if (this.props.activeSection === 'valet') { + self.props.updateSection('valet'); + } else { + self.props.updateSection(''); + } + }, + render: function() { + var clientError = null; + var serverError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + if (this.state.serverError) { + serverError = this.state.serverError; + } + + var valetSection; + var self = this; + + if (this.props.activeSection === 'valet') { + var valetActive = ['', '']; + if (this.state.allowValet === 'false') { + valetActive[1] = 'active'; + } else { + valetActive[0] = 'active'; + } + + var inputs = []; + + function valetActivate() { + self.handleValetRadio('true'); + } + + function valetDeactivate() { + self.handleValetRadio('false'); + } + + inputs.push( +
    +
    + + +
    +

    Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.

    IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.
    +
    + ); + + valetSection = ( + + ); + } else { + var describe = ''; + if (this.state.allowValet === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + valetSection = ( + + ); + } + + return ( +
    +
    + +

    Feature Settings

    +
    +
    +

    Feature Settings

    +
    + {valetSection} +
    +
    +
    + ); + } +}); diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx new file mode 100644 index 0000000000..131add999c --- /dev/null +++ b/web/react/components/team_import_tab.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var SettingUpload = require('./setting_upload.jsx'); + +module.exports = React.createClass({ + displayName: 'Import Tab', + getInitialState: function() { + return {status: 'ready', link: ''}; + }, + onImportFailure: function() { + this.setState({status: 'fail', link: ''}); + }, + onImportSuccess: function(data) { + this.setState({status: 'done', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(data)}); + }, + doImportSlack: function(file) { + this.setState({status: 'in-progress', link: ''}); + utils.importSlack(file, this.onImportSuccess, this.onImportFailure); + }, + render: function() { + var uploadSection = ( + + ); + + var messageSection; + switch (this.state.status) { + case 'ready': + messageSection = ''; + break; + case 'in-progress': + messageSection = ( +

    Importing...

    + ); + break; + case 'done': + messageSection = ( +

    Import sucessfull: View Summery

    + ); + break; + case 'fail': + messageSection = ( +

    Import failure: View Summery

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

    Import

    +
    +
    +

    Import

    +
    + {uploadSection} + {messageSection} +
    +
    +
    + ); + } +}); diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 3bbb5e8925..94d5366512 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -1,161 +1,62 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var SettingPicture = require('./setting_picture.jsx'); +var ImportTab = require('./team_import_tab.jsx'); +var FeatureTab = require('./team_feature_tab.jsx'); var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Constants = require('../utils/constants.jsx'); - -var FeatureTab = React.createClass({ - submitValetFeature: function() { - data = {}; - data['allow_valet'] = this.state.allow_valet; - - client.updateValetFeature(data, - function(data) { - this.props.updateSection(""); - AsyncClient.getMyTeam(); - }.bind(this), - function(err) { - state = this.getInitialState(); - state.server_error = err; - this.setState(state); - }.bind(this) - ); - }, - handleValetRadio: function(val) { - this.setState({ allow_valet: val }); - this.refs.wrapper.getDOMNode().focus(); - }, - componentWillReceiveProps: function(newProps) { - var team = newProps.team; - - var allow_valet = "false"; - if (team && team.allow_valet) { - allow_valet = "true"; - } - - this.setState({ allow_valet: allow_valet }); - }, - getInitialState: function() { - var team = this.props.team; - - var allow_valet = "false"; - if (team && team.allow_valet) { - allow_valet = "true"; - } - - return { allow_valet: allow_valet }; - }, - render: function() { - var team = this.props.team; - - var client_error = this.state.client_error ? this.state.client_error : null; - var server_error = this.state.server_error ? this.state.server_error : null; - - var valetSection; - var self = this; - - if (this.props.activeSection === 'valet') { - var valetActive = ["",""]; - if (this.state.allow_valet === "false") { - valetActive[1] = "active"; - } else { - valetActive[0] = "active"; - } - - var inputs = []; - - inputs.push( -
    -
    - - -
    -

    Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.

    IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.
    -
    - ); - - valetSection = ( - - ); - } else { - var describe = ""; - if (this.state.allow_valet === "false") { - describe = "Off"; - } else { - describe = "On"; - } - - valetSection = ( - - ); - } - - return ( -
    -
    - -

    Feature Settings

    -
    -
    -

    Feature Settings

    -
    - {valetSection} -
    -
    -
    - ); - } -}); - module.exports = React.createClass({ + displayName: 'Team Settings', + propTypes: { + activeTab: React.PropTypes.string.isRequired, + activeSection: React.PropTypes.string.isRequired, + updateSection: React.PropTypes.func.isRequired + }, componentDidMount: function() { - TeamStore.addChangeListener(this._onChange); + TeamStore.addChangeListener(this.onChange); }, componentWillUnmount: function() { - TeamStore.removeChangeListener(this._onChange); + TeamStore.removeChangeListener(this.onChange); }, - _onChange: function () { + onChange: function() { var team = TeamStore.getCurrent(); if (!utils.areStatesEqual(this.state.team, team)) { - this.setState({ team: team }); + this.setState({team: team}); } }, getInitialState: function() { - return { team: TeamStore.getCurrent() }; + return {team: TeamStore.getCurrent()}; }, render: function() { - if (this.props.activeTab === 'general') { - return ( -
    -
    - ); - } else if (this.props.activeTab === 'feature') { - return ( -
    - -
    - ); - } else { - return
    ; + var result; + switch (this.props.activeTab) { + case 'general': + result = ( +
    +
    + ); + break; + case 'feature': + result = ( +
    + +
    + ); + break; + case 'import': + result = ( +
    + +
    + ); + break; + default: + result = ( +
    + ); + break; } + return result; } }); diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index b1c38fd168..c9f479a222 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -5,50 +5,52 @@ var SettingsSidebar = require('./settings_sidebar.jsx'); var TeamSettings = require('./team_settings.jsx'); module.exports = React.createClass({ + displayName: 'Team Settings Modal', componentDidMount: function() { - $('body').on('click', '.modal-back', function(){ + $('body').on('click', '.modal-back', function onClick() { $(this).closest('.modal-dialog').removeClass('display--content'); }); - $('body').on('click', '.modal-header .close', function(){ - setTimeout(function() { + $('body').on('click', '.modal-header .close', function onClick() { + setTimeout(function removeContent() { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); }, updateTab: function(tab) { - this.setState({ active_tab: tab }); + this.setState({activeTab: tab}); }, updateSection: function(section) { - this.setState({ active_section: section }); + this.setState({activeSection: section}); }, getInitialState: function() { - return { active_tab: "feature", active_section: "" }; + return {activeTab: 'feature', activeSection: ''}; }, render: function() { var tabs = []; - tabs.push({name: "feature", ui_name: "Features", icon: "glyphicon glyphicon-wrench"}); + tabs.push({name: 'feature', uiName: 'Features', icon: 'glyphicon glyphicon-wrench'}); + tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'}); return ( -