From 6c4c706313eb765eb00c639f381646be74f27b69 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 25 Apr 2017 11:46:02 -0400 Subject: Start moving webapp to Redux (#6140) * Start moving webapp to Redux * Fix localforage import * Updates per feedback * Feedback udpates and a few fixes * Minor updates * Fix statuses, config not loading properly, getMe sanitizing too much * Fix preferences * Fix user autocomplete * Fix sessions and audits * Fix error handling for all redux actions * Use new directory structure for components and containers * Refresh immediately on logout instead of after timeout * Add fetch polyfill --- api4/system.go | 10 +- api4/user.go | 6 +- webapp/actions/admin_actions.jsx | 23 +- webapp/actions/emoji_actions.jsx | 8 +- webapp/actions/global_actions.jsx | 94 +- webapp/actions/integration_actions.jsx | 12 +- webapp/actions/post_actions.jsx | 8 +- webapp/actions/status_actions.jsx | 21 +- webapp/actions/team_actions.jsx | 9 +- webapp/actions/user_actions.jsx | 459 +++----- webapp/actions/websocket_actions.jsx | 1 - webapp/components/access_history_modal.jsx | 106 -- .../access_history_modal/access_history_modal.jsx | 108 ++ webapp/components/access_history_modal/index.js | 24 + webapp/components/activity_log_modal.jsx | 308 ----- .../activity_log_modal/activity_log_modal.jsx | 301 +++++ webapp/components/activity_log_modal/index.js | 25 + webapp/components/add_users_to_team.jsx | 278 ----- .../add_users_to_team/add_users_to_team.jsx | 257 +++++ webapp/components/add_users_to_team/index.js | 24 + .../admin_console/system_users/system_users.jsx | 87 +- webapp/components/channel_header.jsx | 4 +- webapp/components/channel_invite_modal.jsx | 197 ---- .../channel_invite_modal/channel_invite_modal.jsx | 188 +++ webapp/components/channel_invite_modal/index.js | 24 + webapp/components/login/login_controller.jsx | 22 +- webapp/components/member_list_channel.jsx | 47 +- webapp/components/member_list_team.jsx | 30 +- webapp/components/more_direct_channels.jsx | 341 ------ webapp/components/more_direct_channels/index.js | 25 + .../more_direct_channels/more_direct_channels.jsx | 328 ++++++ webapp/components/navbar.jsx | 2 +- webapp/components/popover_list_members.jsx | 279 ----- webapp/components/popover_list_members/index.js | 24 + .../popover_list_members/popover_list_members.jsx | 282 +++++ webapp/components/root.jsx | 1 + webapp/components/sidebar.jsx | 2 +- webapp/components/sidebar_header_dropdown.jsx | 2 +- webapp/components/sidebar_right_menu.jsx | 2 +- .../components/signup/components/signup_email.jsx | 6 +- .../components/signup/components/signup_ldap.jsx | 4 +- webapp/components/signup/signup_controller.jsx | 3 +- .../components/suggestion/at_mention_provider.jsx | 8 +- .../components/suggestion/search_user_provider.jsx | 2 +- .../suggestion/switch_channel_provider.jsx | 4 +- webapp/components/team_members_dropdown.jsx | 392 ------- webapp/components/team_members_dropdown/index.js | 24 + .../team_members_dropdown.jsx | 395 +++++++ webapp/components/user_settings/user_settings.jsx | 4 +- .../user_settings/user_settings_general.jsx | 1207 ------------------- .../user_settings/user_settings_general/index.js | 24 + .../user_settings_general.jsx | 1209 ++++++++++++++++++++ .../user_settings/user_settings_security.jsx | 1033 ----------------- .../user_settings/user_settings_security/index.js | 24 + .../user_settings_security.jsx | 1036 +++++++++++++++++ webapp/package.json | 13 +- webapp/root.jsx | 43 +- webapp/store/index.js | 112 ++ webapp/store/utils.js | 42 + webapp/stores/preference_store.jsx | 42 +- webapp/stores/redux_store.jsx | 19 + webapp/stores/team_store.jsx | 82 +- webapp/stores/user_store.jsx | 543 ++------- webapp/utils/async_client.jsx | 226 ---- webapp/utils/channel_intro_messages.jsx | 2 +- webapp/webpack.config.js | 4 +- 66 files changed, 5083 insertions(+), 5389 deletions(-) delete mode 100644 webapp/components/access_history_modal.jsx create mode 100644 webapp/components/access_history_modal/access_history_modal.jsx create mode 100644 webapp/components/access_history_modal/index.js delete mode 100644 webapp/components/activity_log_modal.jsx create mode 100644 webapp/components/activity_log_modal/activity_log_modal.jsx create mode 100644 webapp/components/activity_log_modal/index.js delete mode 100644 webapp/components/add_users_to_team.jsx create mode 100644 webapp/components/add_users_to_team/add_users_to_team.jsx create mode 100644 webapp/components/add_users_to_team/index.js delete mode 100644 webapp/components/channel_invite_modal.jsx create mode 100644 webapp/components/channel_invite_modal/channel_invite_modal.jsx create mode 100644 webapp/components/channel_invite_modal/index.js delete mode 100644 webapp/components/more_direct_channels.jsx create mode 100644 webapp/components/more_direct_channels/index.js create mode 100644 webapp/components/more_direct_channels/more_direct_channels.jsx delete mode 100644 webapp/components/popover_list_members.jsx create mode 100644 webapp/components/popover_list_members/index.js create mode 100644 webapp/components/popover_list_members/popover_list_members.jsx delete mode 100644 webapp/components/team_members_dropdown.jsx create mode 100644 webapp/components/team_members_dropdown/index.js create mode 100644 webapp/components/team_members_dropdown/team_members_dropdown.jsx delete mode 100644 webapp/components/user_settings/user_settings_general.jsx create mode 100644 webapp/components/user_settings/user_settings_general/index.js create mode 100644 webapp/components/user_settings/user_settings_general/user_settings_general.jsx delete mode 100644 webapp/components/user_settings/user_settings_security.jsx create mode 100644 webapp/components/user_settings/user_settings_security/index.js create mode 100644 webapp/components/user_settings/user_settings_security/user_settings_security.jsx create mode 100644 webapp/store/index.js create mode 100644 webapp/store/utils.js create mode 100644 webapp/stores/redux_store.jsx diff --git a/api4/system.go b/api4/system.go index 55be559bf..7e860ba76 100644 --- a/api4/system.go +++ b/api4/system.go @@ -5,6 +5,7 @@ package api4 import ( "net/http" + "strconv" l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/app" @@ -202,7 +203,14 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { return } - w.Write([]byte(model.MapToJson(utils.ClientCfg))) + respCfg := map[string]string{} + for k, v := range utils.ClientCfg { + respCfg[k] = v + } + + respCfg["NoAccounts"] = strconv.FormatBool(app.IsFirstUserAccount()) + + w.Write([]byte(model.MapToJson(respCfg))) } func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/user.go b/api4/user.go index 1c870f1c1..1d117ce07 100644 --- a/api4/user.go +++ b/api4/user.go @@ -111,7 +111,11 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { if HandleEtag(etag, "Get User", w, r) { return } else { - app.SanitizeProfile(user, c.IsSystemAdmin()) + if c.Session.UserId == user.Id { + user.Sanitize(map[string]bool{}) + } else { + app.SanitizeProfile(user, c.IsSystemAdmin()) + } w.Header().Set(model.HEADER_ETAG_SERVER, etag) w.Write([]byte(user.ToJson())) return diff --git a/webapp/actions/admin_actions.jsx b/webapp/actions/admin_actions.jsx index ee3d6fd8a..9a522caf9 100644 --- a/webapp/actions/admin_actions.jsx +++ b/webapp/actions/admin_actions.jsx @@ -5,21 +5,12 @@ import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import {browserHistory} from 'react-router/es6'; -export function revokeSession(altId, success, error) { - Client.revokeSession(altId, - () => { - AsyncClient.getSessions(); - if (success) { - success(); - } - }, - (err) => { - if (error) { - error(err); - } - } - ); -} +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; + +import {getUser} from 'mattermost-redux/actions/users'; export function saveConfig(config, success, error) { Client.saveConfig( @@ -57,7 +48,7 @@ export function adminResetMfa(userId, success, error) { Client.adminResetMfa( userId, () => { - AsyncClient.getUser(userId); + getUser(userId)(dispatch, getState); if (success) { success(); diff --git a/webapp/actions/emoji_actions.jsx b/webapp/actions/emoji_actions.jsx index feb6bd76b..ed8bc84f7 100644 --- a/webapp/actions/emoji_actions.jsx +++ b/webapp/actions/emoji_actions.jsx @@ -10,6 +10,12 @@ import Client from 'client/web_client.jsx'; import {ActionTypes} from 'utils/constants.jsx'; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; +import {getProfilesByIds} from 'mattermost-redux/actions/users'; + export function loadEmoji(getProfiles = true) { Client.listEmoji( (data) => { @@ -42,5 +48,5 @@ function loadProfilesForEmoji(emojiList) { return; } - AsyncClient.getProfilesByIds(list); + getProfilesByIds(list)(dispatch, getState); } diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index c3cfb00a4..bd9178604 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -9,7 +9,6 @@ import UserStore from 'stores/user_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import PreferenceStore from 'stores/preference_store.jsx'; import SearchStore from 'stores/search_store.jsx'; import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx'; @@ -32,6 +31,12 @@ import en from 'i18n/en.json'; import * as I18n from 'i18n/i18n.jsx'; import {browserHistory} from 'react-router/es6'; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; +import {ChannelTypes} from 'mattermost-redux/action_types'; + export function emitChannelClickEvent(channel) { function userVisitedFakeChannel(chan, success, fail) { const otherUserId = Utils.getUserIdFromChannelName(chan); @@ -77,6 +82,11 @@ export function emitChannelClickEvent(channel) { channelMember, prev: oldChannelId }); + + dispatch({ + type: ChannelTypes.SELECT_CHANNEL, + data: chan.id + }, getState); } if (channel.fake) { @@ -94,85 +104,6 @@ export function emitChannelClickEvent(channel) { } } -export function emitInitialLoad(callback) { - Client.getInitialLoad( - (data) => { - global.window.mm_config = data.client_cfg; - global.window.mm_license = data.license_cfg; - - if (global.window && global.window.analytics) { - global.window.analytics.identify(global.window.mm_config.DiagnosticId, {}, { - context: { - ip: '0.0.0.0' - }, - page: { - path: '', - referrer: '', - search: '', - title: '', - url: '' - }, - anonymousId: '00000000000000000000000000' - }); - } - - UserStore.setNoAccounts(data.no_accounts); - - if (data.user && data.user.id) { - global.window.mm_user = data.user; - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_ME, - me: data.user - }); - } - - if (data.preferences) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PREFERENCES, - preferences: data.preferences - }); - } - - if (data.teams) { - var teams = {}; - data.teams.forEach((team) => { - teams[team.id] = team; - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_ALL_TEAMS, - teams - }); - } - - if (data.team_members) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS, - team_members: data.team_members - }); - } - - if (data.direct_profiles) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_DIRECT_PROFILES, - profiles: data.direct_profiles - }); - } - - if (callback) { - callback(); - } - }, - (err) => { - AsyncClient.dispatchError(err, 'getInitialLoad'); - - if (callback) { - callback(); - } - } - ); -} - export function doFocusPost(channelId, postId, data) { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_FOCUSED_POST, @@ -536,12 +467,11 @@ export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = tr export function clientLogout(redirectTo = '/') { BrowserStore.clear(); ErrorStore.clearLastError(); - PreferenceStore.clear(); - UserStore.clear(); TeamStore.clear(); ChannelStore.clear(); stopPeriodicStatusUpdates(); WebsocketActions.close(); + localStorage.removeItem('currentUserId'); window.location.href = redirectTo; } diff --git a/webapp/actions/integration_actions.jsx b/webapp/actions/integration_actions.jsx index 43a4c75f4..c1bbf3432 100644 --- a/webapp/actions/integration_actions.jsx +++ b/webapp/actions/integration_actions.jsx @@ -11,6 +11,12 @@ import Client from 'client/web_client.jsx'; import {ActionTypes} from 'utils/constants.jsx'; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; +import {getProfilesByIds} from 'mattermost-redux/actions/users'; + export function loadIncomingHooks() { Client.listIncomingHooks( (data) => { @@ -42,7 +48,7 @@ function loadProfilesForIncomingHooks(hooks) { return; } - AsyncClient.getProfilesByIds(list); + getProfilesByIds(list)(dispatch, getState); } export function loadOutgoingHooks() { @@ -76,7 +82,7 @@ function loadProfilesForOutgoingHooks(hooks) { return; } - AsyncClient.getProfilesByIds(list); + getProfilesByIds(list)(dispatch, getState); } export function loadTeamCommands() { @@ -110,5 +116,5 @@ function loadProfilesForCommands(commands) { return; } - AsyncClient.getProfilesByIds(list); + getProfilesByIds(list)(dispatch, getState); } diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index 266370f60..36abfc2be 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -20,6 +20,12 @@ import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; const Preferences = Constants.Preferences; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; +import {getProfilesByIds} from 'mattermost-redux/actions/users'; + export function handleNewPost(post, msg) { let websocketMessageProps = {}; if (msg) { @@ -310,7 +316,7 @@ export function loadProfilesForPosts(posts) { return; } - AsyncClient.getProfilesByIds(list); + getProfilesByIds(list)(dispatch, getState); } export function addReaction(channelId, postId, emojiName) { diff --git a/webapp/actions/status_actions.jsx b/webapp/actions/status_actions.jsx index 649df835a..066a89254 100644 --- a/webapp/actions/status_actions.jsx +++ b/webapp/actions/status_actions.jsx @@ -1,16 +1,19 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; - import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import UserStore from 'stores/user_store.jsx'; -import Client from 'client/web_client.jsx'; +import {Preferences, Constants} from 'utils/constants.jsx'; + +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; -import {ActionTypes, Preferences, Constants} from 'utils/constants.jsx'; +import {getStatusesByIds} from 'mattermost-redux/actions/users'; export function loadStatusesForChannel(channelId = ChannelStore.getCurrentId()) { const postList = PostStore.getVisiblePosts(channelId); @@ -108,15 +111,7 @@ export function loadStatusesByIds(userIds) { return; } - Client.getStatusesByIds( - userIds, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_STATUSES, - statuses: data - }); - } - ); + getStatusesByIds(userIds)(dispatch, getState); } let intervalId = ''; diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 1dcfecbab..f263108fd 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -13,6 +13,13 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import {browserHistory} from 'react-router/es6'; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; + +import {getUser} from 'mattermost-redux/actions/users'; + export function checkIfTeamExists(teamName, onSuccess, onError) { Client.findTeamByName(teamName, onSuccess, onError); } @@ -62,7 +69,7 @@ export function removeUserFromTeam(teamId, userId, success, error) { TeamStore.removeMemberInTeam(teamId, userId); UserStore.removeProfileFromTeam(teamId, userId); UserStore.emitInTeamChange(); - AsyncClient.getUser(userId); + getUser(userId)(dispatch, getState); AsyncClient.getTeamStats(teamId); if (success) { diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index 9f9987cdd..8a794bb0a 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -19,6 +19,79 @@ import Client from 'client/web_client.jsx'; import {Constants, ActionTypes, Preferences} from 'utils/constants.jsx'; import {browserHistory} from 'react-router/es6'; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; + +import { + getProfiles, + getProfilesInChannel, + getProfilesInTeam, + getProfilesWithoutTeam, + getProfilesByIds, + getMe, + searchProfiles, + autocompleteUsers as autocompleteRedux, + updateMe, + updateUserMfa, + checkMfa as checkMfaRedux, + updateUserPassword, + createUser, + login, + loadMe as loadMeRedux +} from 'mattermost-redux/actions/users'; + +import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; + +export function loadMe(callback) { + loadMeRedux()(dispatch, getState).then( + () => { + localStorage.setItem('currentUserId', UserStore.getCurrentId()); + + if (callback) { + callback(); + } + } + ); +} + +export function loadMeAndConfig(callback) { + loadMe(() => { + getClientConfig()(store.dispatch, store.getState).then( + (config) => { + global.window.mm_config = config; + + if (global.window && global.window.analytics) { + global.window.analytics.identify(global.window.mm_config.DiagnosticId, {}, { + context: { + ip: '0.0.0.0' + }, + page: { + path: '', + referrer: '', + search: '', + title: '', + url: '' + }, + anonymousId: '00000000000000000000000000' + }); + } + + getLicenseConfig()(store.dispatch, store.getState).then( + (license) => { // eslint-disable-line max-nested-callbacks + global.window.mm_license = license; + + if (callback) { + callback(); + } + } + ); + } + ); + }); +} + export function switchFromLdapToEmail(email, password, token, ldapPassword, onSuccess, onError) { Client.ldapToEmail( email, @@ -38,80 +111,30 @@ export function switchFromLdapToEmail(email, password, token, ldapPassword, onSu ); } -export function loadProfilesAndTeamMembers(offset, limit, teamId = TeamStore.getCurrentId(), success, error) { - Client.getProfilesInTeam( - teamId, - offset, - limit, +export function loadProfilesAndTeamMembers(page, perPage, teamId = TeamStore.getCurrentId(), success) { + getProfilesInTeam(teamId, page, perPage)(dispatch, getState).then( (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_IN_TEAM, - profiles: data, - team_id: teamId, - offset, - count: Object.keys(data).length - }); - - loadTeamMembersForProfilesMap(data, teamId, success, error); - loadStatusesForProfilesMap(data); - }, - (err) => { - AsyncClient.dispatchError(err, 'getProfilesInTeam'); + loadTeamMembersForProfilesList(data, teamId, success); + loadStatusesForProfilesList(data); } ); } -export function loadProfilesAndTeamMembersAndChannelMembers(offset, limit, teamId = TeamStore.getCurrentId(), channelId = ChannelStore.getCurrentId(), success, error) { - Client.getProfilesInChannel( - channelId, - offset, - limit, +export function loadProfilesAndTeamMembersAndChannelMembers(page, perPage, teamId = TeamStore.getCurrentId(), channelId = ChannelStore.getCurrentId(), success, error) { + getProfilesInChannel(channelId, page, perPage)(dispatch, getState).then( (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_IN_CHANNEL, - profiles: data, - channel_id: channelId, - offset, - count: Object.keys(data).length - }); - - loadTeamMembersForProfilesMap( + loadTeamMembersForProfilesList( data, teamId, () => { - loadChannelMembersForProfilesMap(data, channelId, success, error); - loadStatusesForProfilesMap(data); - }); - }, - (err) => { - AsyncClient.dispatchError(err, 'getProfilesInChannel'); + loadChannelMembersForProfilesList(data, channelId, success, error); + loadStatusesForProfilesList(data); + } + ); } ); } -export function loadTeamMembersForProfilesMap(profiles, teamId = TeamStore.getCurrentId(), success, error) { - const membersToLoad = {}; - for (const pid in profiles) { - if (!profiles.hasOwnProperty(pid)) { - continue; - } - - if (!TeamStore.hasActiveMemberInTeam(teamId, pid)) { - membersToLoad[pid] = true; - } - } - - const list = Object.keys(membersToLoad); - if (list.length === 0) { - if (success) { - success({}); - } - return; - } - - loadTeamMembersForProfiles(list, teamId, success, error); -} - export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getCurrentId(), success, error) { const membersToLoad = {}; for (let i = 0; i < profiles.length; i++) { @@ -133,24 +156,13 @@ export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getC loadTeamMembersForProfiles(list, teamId, success, error); } -export function loadProfilesWithoutTeam(page, perPage, success, error) { - Client.getProfilesWithoutTeam( - page, - perPage, +export function loadProfilesWithoutTeam(page, perPage, success) { + getProfilesWithoutTeam(page, perPage)(dispatch, getState).then( (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_WITHOUT_TEAM, - profiles: data, - page - }); - loadStatusesForProfilesMap(data); - }, - (err) => { - AsyncClient.dispatchError(err, 'getProfilesWithoutTeam'); - if (error) { - error(err); + if (success) { + success(data); } } ); @@ -248,9 +260,9 @@ function populateDMChannelsWithProfiles(userIds) { } } -function populateChannelWithProfiles(channelId, userIds) { - for (let i = 0; i < userIds.length; i++) { - UserStore.saveUserIdInChannel(channelId, userIds[i]); +function populateChannelWithProfiles(channelId, users) { + for (let i = 0; i < users.length; i++) { + UserStore.saveUserIdInChannel(channelId, users[i].id); } UserStore.emitInChannelChange(); } @@ -360,17 +372,9 @@ export function loadProfilesForGM() { }); } - Client.getProfilesInChannel( - channel.id, - 0, - Constants.MAX_USERS_IN_GM, + getProfilesInChannel(channel.id, 0, Constants.MAX_USERS_IN_GM)(dispatch, getState).then( (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - - populateChannelWithProfiles(channel.id, Object.keys(data)); + populateChannelWithProfiles(channel.id, data); } ); } @@ -420,20 +424,10 @@ export function loadProfilesForDM() { } if (profilesToLoad.length > 0) { - Client.getProfilesByIds( - profilesToLoad, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - - // Use membersToLoad so we get all the DM profiles even if they were already loaded + getProfilesByIds(profilesToLoad)(dispatch, getState).then( + () => { populateDMChannelsWithProfiles(profileIds); }, - (err) => { - AsyncClient.dispatchError(err, 'getProfilesByIds'); - } ); } else { populateDMChannelsWithProfiles(profileIds); @@ -491,119 +485,70 @@ function onThemeSaved(teamId, theme, onSuccess) { onSuccess(); } -export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) { - Client.searchUsers( - term, - teamId, - options, +export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = {}, success) { + searchProfiles(term, {team_id: teamId, ...options})(dispatch, getState).then( (data) => { loadStatusesForProfilesList(data); if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'searchUsers'); - - if (error) { - error(err); - } } ); } -export function searchUsersNotInTeam(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) { - Client.searchUsersNotInTeam( - term, - teamId, - options, +export function searchUsersNotInTeam(term, teamId = TeamStore.getCurrentId(), options = {}, success) { + searchProfiles(term, {not_in_team_id: teamId, ...options})(dispatch, getState).then( (data) => { loadStatusesForProfilesList(data); if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'searchUsersNotInTeam'); - - if (error) { - error(err); - } } ); } -export function autocompleteUsersInChannel(username, channelId, success, error) { - Client.autocompleteUsersInChannel( - username, - channelId, +export function autocompleteUsersInChannel(username, channelId, success) { + const channel = ChannelStore.get(channelId); + const teamId = channel ? channel.team_id : TeamStore.getCurrentId(); + autocompleteRedux(username, teamId, channelId)(dispatch, getState).then( (data) => { if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'autocompleteUsersInChannel'); - - if (error) { - error(err); - } } ); } -export function autocompleteUsersInTeam(username, success, error) { - Client.autocompleteUsersInTeam( - username, +export function autocompleteUsersInTeam(username, success) { + autocompleteRedux(username, TeamStore.getCurrentId())(dispatch, getState).then( (data) => { if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'autocompleteUsersInTeam'); - - if (error) { - error(err); - } } ); } -export function autocompleteUsers(username, success, error) { - Client.autocompleteUsers( - username, +export function autocompleteUsers(username, success) { + autocompleteRedux(username)(dispatch, getState).then( (data) => { if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'autocompleteUsers'); - - if (error) { - error(err); - } } ); } -export function updateUser(username, type, success, error) { - Client.updateUser( - username, - type, +export function updateUser(user, type, success, error) { + updateMe(user)(dispatch, getState).then( (data) => { - if (success) { + if (data && success) { success(data); - } - }, - (err) => { - if (error) { - error(err); - } else { - AsyncClient.dispatchError(err, 'updateUser'); + } else if (data == null && error) { + const serverError = getState().requests.users.updateUser.error; + error({id: serverError.server_error_id, ...serverError}); } } ); @@ -626,74 +571,55 @@ export function generateMfaSecret(success, error) { ); } -export function updateUserNotifyProps(data, success, error) { - Client.updateUserNotifyProps( - data, - () => { - AsyncClient.getMe(); - - if (success) { - success(); - } - }, - (err) => { - if (error) { - error(err); - } - } +export function updateUserNotifyProps(props, success, error) { + updateMe({notify_props: props})(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.updateMe.error; + error({id: serverError.server_error_id, ...serverError}); + } + } ); } export function updateUserRoles(userId, newRoles, success, error) { - Client.updateUserRoles( - userId, - newRoles, - () => { - AsyncClient.getUser( - userId, - success, - error - ); - }, - error + updateUserRoles(userId, newRoles)(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.updateUser.error; + error({id: serverError.server_error_id, ...serverError}); + } + } ); } export function activateMfa(code, success, error) { - Client.updateMfa( - code, - true, - () => { - AsyncClient.getMe(); - - if (success) { - success(); + updateUserMfa(UserStore.getCurrentId(), true, code)(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.updateUser.error; + error({id: serverError.server_error_id, ...serverError}); } }, - (err) => { - if (error) { - error(err); - } - } ); } export function deactivateMfa(success, error) { - Client.updateMfa( - '', - false, - () => { - AsyncClient.getMe(); - - if (success) { - success(); + updateUserMfa(UserStore.getCurrentId(), false)(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.updateUser.error; + error({id: serverError.server_error_id, ...serverError}); } }, - (err) => { - if (error) { - error(err); - } - } ); } @@ -703,16 +629,13 @@ export function checkMfa(loginId, success, error) { return; } - Client.checkMfa( - loginId, + checkMfaRedux(loginId)(dispatch, getState).then( (data) => { - if (success) { - success(data && data.mfa_required === 'true'); - } - }, - (err) => { - if (error) { - error(err); + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.checkMfa.error; + error({id: serverError.server_error_id, ...serverError}); } } ); @@ -735,15 +658,13 @@ export function updateActive(userId, active, success, error) { } export function updatePassword(userId, currentPassword, newPassword, success, error) { - Client.updatePassword(userId, currentPassword, newPassword, - () => { - if (success) { - success(); - } - }, - (err) => { - if (error) { - error(err); + updateUserPassword(userId, currentPassword, newPassword)(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.users.updateUser.error; + error({id: serverError.server_error_id, ...serverError}); } } ); @@ -820,37 +741,27 @@ export function loginById(userId, password, mfaToken, success, error) { } export function createUserWithInvite(user, data, emailHash, inviteId, success, error) { - Client.createUserWithInvite( - user, - data, - emailHash, - inviteId, - (response) => { - if (success) { - success(response); - } - }, - (err) => { - if (error) { - error(err); + createUser(user, data, emailHash, inviteId)(dispatch, getState).then( + (resp) => { + if (resp && success) { + success(resp); + } else if (resp == null && error) { + const serverError = getState().requests.users.create.error; + error({id: serverError.server_error_id, ...serverError}); } } ); } export function webLogin(loginId, password, token, success, error) { - Client.webLogin( - loginId, - password, - token, - () => { - if (success) { + login(loginId, password, token)(dispatch, getState).then( + (ok) => { + if (ok && success) { + localStorage.setItem('currentUserId', UserStore.getCurrentId()); success(); - } - }, - (err) => { - if (error) { - error(err); + } else if (!ok && error) { + const serverError = getState().requests.users.login.error; + error({id: serverError.server_error_id, ...serverError}); } } ); @@ -907,7 +818,7 @@ export function uploadProfileImage(userPicture, success, error) { Client.uploadProfileImage( userPicture, () => { - AsyncClient.getMe(); + getMe()(dispatch, getState); if (success) { success(); } @@ -920,38 +831,24 @@ export function uploadProfileImage(userPicture, success, error) { ); } -export function loadProfiles(offset = UserStore.getPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE, success, error) { - Client.getProfiles( - offset, - limit, +export function loadProfiles(page, perPage, success) { + getProfiles(page, perPage)(dispatch, getState).then( (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - if (success) { success(data); } - }, - (err) => { - AsyncClient.dispatchError(err, 'getProfiles'); - - if (error) { - error(err); - } } ); } -export function getMissingProfiles(ids, success, error) { +export function getMissingProfiles(ids) { const missingIds = ids.filter((id) => !UserStore.hasProfile(id)); if (missingIds.length === 0) { return; } - AsyncClient.getProfilesByIds(missingIds, success, error); + getProfilesByIds(missingIds)(dispatch, getState); } export function loadMyTeamMembers() { diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index 41e1c8f4b..bd220947a 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -338,7 +338,6 @@ function handleUserUpdatedEvent(msg) { const user = msg.data.user; if (UserStore.getCurrentId() !== user.id) { UserStore.saveProfile(user); - UserStore.emitChange(user.id); } } diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx deleted file mode 100644 index 25c7ef380..000000000 --- a/webapp/components/access_history_modal.jsx +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import LoadingScreen from './loading_screen.jsx'; -import AuditTable from './audit_table.jsx'; - -import UserStore from 'stores/user_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import $ from 'jquery'; -import React from 'react'; - -import {Modal} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; - -export default class AccessHistoryModal extends React.Component { - constructor(props) { - super(props); - - this.onAuditChange = this.onAuditChange.bind(this); - this.onShow = this.onShow.bind(this); - this.onHide = this.onHide.bind(this); - - const state = this.getStateFromStoresForAudits(); - state.moreInfo = []; - state.show = true; - - this.state = state; - } - - getStateFromStoresForAudits() { - return { - audits: UserStore.getAudits() - }; - } - - onShow() { - AsyncClient.getAudits(); - if (!Utils.isMobile()) { - $('.modal-body').perfectScrollbar(); - } - } - - onHide() { - this.setState({show: false}); - } - - componentDidMount() { - UserStore.addAuditsChangeListener(this.onAuditChange); - this.onShow(); - } - - componentWillUnmount() { - UserStore.removeAuditsChangeListener(this.onAuditChange); - } - - onAuditChange() { - const newState = this.getStateFromStoresForAudits(); - if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) { - this.setState(newState); - } - } - - render() { - let content; - if (this.state.audits.length === 0) { - content = (); - } else { - content = ( - - ); - } - - return ( - - - - - - - - {content} - - - ); - } -} - -AccessHistoryModal.propTypes = { - onHide: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/access_history_modal/access_history_modal.jsx b/webapp/components/access_history_modal/access_history_modal.jsx new file mode 100644 index 000000000..da03fdb5b --- /dev/null +++ b/webapp/components/access_history_modal/access_history_modal.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from 'components/loading_screen.jsx'; +import AuditTable from 'components/audit_table.jsx'; + +import UserStore from 'stores/user_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +import $ from 'jquery'; +import React from 'react'; + +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +export default class AccessHistoryModal extends React.Component { + static propTypes = { + onHide: React.PropTypes.func.isRequired, + actions: React.PropTypes.shape({ + getUserAudits: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.onAuditChange = this.onAuditChange.bind(this); + this.onShow = this.onShow.bind(this); + this.onHide = this.onHide.bind(this); + + const state = this.getStateFromStoresForAudits(); + state.moreInfo = []; + state.show = true; + + this.state = state; + } + + getStateFromStoresForAudits() { + return { + audits: UserStore.getAudits() + }; + } + + onShow() { + this.props.actions.getUserAudits(UserStore.getCurrentId(), 0, 200); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); + } + } + + onHide() { + this.setState({show: false}); + } + + componentDidMount() { + UserStore.addAuditsChangeListener(this.onAuditChange); + this.onShow(); + } + + componentWillUnmount() { + UserStore.removeAuditsChangeListener(this.onAuditChange); + } + + onAuditChange() { + const newState = this.getStateFromStoresForAudits(); + if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) { + this.setState(newState); + } + } + + render() { + let content; + if (this.state.audits.length === 0) { + content = (); + } else { + content = ( + + ); + } + + return ( + + + + + + + + {content} + + + ); + } +} diff --git a/webapp/components/access_history_modal/index.js b/webapp/components/access_history_modal/index.js new file mode 100644 index 000000000..4842ca730 --- /dev/null +++ b/webapp/components/access_history_modal/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getUserAudits} from 'mattermost-redux/actions/users'; + +import AccessHistoryModal from './access_history_modal.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getUserAudits + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AccessHistoryModal); diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx deleted file mode 100644 index 8890a1d19..000000000 --- a/webapp/components/activity_log_modal.jsx +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import LoadingScreen from './loading_screen.jsx'; - -import UserStore from 'stores/user_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import $ from 'jquery'; -import React from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; - -import {revokeSession} from 'actions/admin_actions.jsx'; - -export default class ActivityLogModal extends React.Component { - constructor(props) { - super(props); - - this.submitRevoke = this.submitRevoke.bind(this); - this.onListenerChange = this.onListenerChange.bind(this); - this.handleMoreInfo = this.handleMoreInfo.bind(this); - this.onHide = this.onHide.bind(this); - this.onShow = this.onShow.bind(this); - - const state = this.getStateFromStores(); - state.moreInfo = []; - state.show = true; - - this.state = state; - } - - getStateFromStores() { - return { - sessions: UserStore.getSessions(), - serverError: null, - clientError: null - }; - } - - submitRevoke(altId, e) { - e.preventDefault(); - var modalContent = $(e.target).closest('.modal-content'); - modalContent.addClass('animation--highlight'); - setTimeout(() => { - modalContent.removeClass('animation--highlight'); - }, 1500); - revokeSession(altId, - null, - (err) => { - const state = this.getStateFromStores(); - state.serverError = err; - this.setState(state); - } - ); - } - - onShow() { - AsyncClient.getSessions(); - if (!Utils.isMobile()) { - $('.modal-body').perfectScrollbar(); - } - } - - onHide() { - this.setState({show: false}); - } - - componentDidMount() { - UserStore.addSessionsChangeListener(this.onListenerChange); - this.onShow(); - } - - componentWillUnmount() { - UserStore.removeSessionsChangeListener(this.onListenerChange); - } - - onListenerChange() { - const newState = this.getStateFromStores(); - if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) { - this.setState(newState); - } - } - - handleMoreInfo(index) { - const newMoreInfo = this.state.moreInfo; - newMoreInfo[index] = true; - this.setState({moreInfo: newMoreInfo}); - } - - render() { - const activityList = []; - - for (let i = 0; i < this.state.sessions.length; i++) { - const currentSession = this.state.sessions[i]; - const lastAccessTime = new Date(currentSession.last_activity_at); - const firstAccessTime = new Date(currentSession.create_at); - let devicePlatform = currentSession.props.platform; - let devicePicture = ''; - - if (currentSession.props.platform === 'Windows') { - devicePicture = 'fa fa-windows'; - } else if (currentSession.device_id && currentSession.device_id.indexOf('apple') === 0) { - devicePicture = 'fa fa-apple'; - devicePlatform = ( - - ); - } else if (currentSession.device_id && currentSession.device_id.indexOf('android') === 0) { - devicePlatform = ( - - ); - devicePicture = 'fa fa-android'; - } else if (currentSession.props.platform === 'Macintosh' || - currentSession.props.platform === 'iPhone') { - devicePicture = 'fa fa-apple'; - } else if (currentSession.props.platform === 'Linux') { - if (currentSession.props.os.indexOf('Android') >= 0) { - devicePlatform = ( - - ); - devicePicture = 'fa fa-android'; - } else { - devicePicture = 'fa fa-linux'; - } - } else if (currentSession.props.os.indexOf('Linux') !== -1) { - devicePicture = 'fa fa-linux'; - } - - if (currentSession.props.browser.indexOf('Desktop App') !== -1) { - devicePlatform = ( - - ); - } - - let moreInfo; - if (this.state.moreInfo[i]) { - moreInfo = ( -
-
- - ), - time: ( - - ) - }} - /> -
-
- -
-
- -
-
- -
-
- ); - } else { - moreInfo = ( - - - - ); - } - - activityList[i] = ( -
-
-
{devicePlatform}
-
-
- - ), - time: ( - - ) - }} - /> -
- {moreInfo} -
-
-
- -
-
- ); - } - - let content; - if (this.state.sessions.loading) { - content = ; - } else { - content =
{activityList}
; - } - - return ( - - - - - - - -

- -

- {content} -
-
- ); - } -} - -ActivityLogModal.propTypes = { - onHide: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/activity_log_modal/activity_log_modal.jsx b/webapp/components/activity_log_modal/activity_log_modal.jsx new file mode 100644 index 000000000..c94909754 --- /dev/null +++ b/webapp/components/activity_log_modal/activity_log_modal.jsx @@ -0,0 +1,301 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from 'components/loading_screen.jsx'; + +import UserStore from 'stores/user_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +import $ from 'jquery'; +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; + +export default class ActivityLogModal extends React.Component { + static propTypes = { + onHide: React.PropTypes.func.isRequired, + actions: React.PropTypes.shape({ + getSessions: React.PropTypes.func.isRequired, + revokeSession: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.submitRevoke = this.submitRevoke.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + this.handleMoreInfo = this.handleMoreInfo.bind(this); + this.onHide = this.onHide.bind(this); + this.onShow = this.onShow.bind(this); + + const state = this.getStateFromStores(); + state.moreInfo = []; + state.show = true; + + this.state = state; + } + + getStateFromStores() { + return { + sessions: UserStore.getSessions(), + clientError: null + }; + } + + submitRevoke(altId, e) { + e.preventDefault(); + var modalContent = $(e.target).closest('.modal-content'); + modalContent.addClass('animation--highlight'); + setTimeout(() => { + modalContent.removeClass('animation--highlight'); + }, 1500); + this.props.actions.revokeSession(UserStore.getCurrentId(), altId); + } + + onShow() { + this.props.actions.getSessions(UserStore.getCurrentId()); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); + } + } + + onHide() { + this.setState({show: false}); + } + + componentDidMount() { + UserStore.addSessionsChangeListener(this.onListenerChange); + this.onShow(); + } + + componentWillUnmount() { + UserStore.removeSessionsChangeListener(this.onListenerChange); + } + + onListenerChange() { + const newState = this.getStateFromStores(); + if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) { + this.setState(newState); + } + } + + handleMoreInfo(index) { + const newMoreInfo = this.state.moreInfo; + newMoreInfo[index] = true; + this.setState({moreInfo: newMoreInfo}); + } + + render() { + const activityList = []; + + for (let i = 0; i < this.state.sessions.length; i++) { + const currentSession = this.state.sessions[i]; + const lastAccessTime = new Date(currentSession.last_activity_at); + const firstAccessTime = new Date(currentSession.create_at); + let devicePlatform = currentSession.props.platform; + let devicePicture = ''; + + if (currentSession.props.platform === 'Windows') { + devicePicture = 'fa fa-windows'; + } else if (currentSession.device_id && currentSession.device_id.indexOf('apple') === 0) { + devicePicture = 'fa fa-apple'; + devicePlatform = ( + + ); + } else if (currentSession.device_id && currentSession.device_id.indexOf('android') === 0) { + devicePlatform = ( + + ); + devicePicture = 'fa fa-android'; + } else if (currentSession.props.platform === 'Macintosh' || + currentSession.props.platform === 'iPhone') { + devicePicture = 'fa fa-apple'; + } else if (currentSession.props.platform === 'Linux') { + if (currentSession.props.os.indexOf('Android') >= 0) { + devicePlatform = ( + + ); + devicePicture = 'fa fa-android'; + } else { + devicePicture = 'fa fa-linux'; + } + } else if (currentSession.props.os.indexOf('Linux') !== -1) { + devicePicture = 'fa fa-linux'; + } + + if (currentSession.props.browser.indexOf('Desktop App') !== -1) { + devicePlatform = ( + + ); + } + + let moreInfo; + if (this.state.moreInfo[i]) { + moreInfo = ( +
+
+ + ), + time: ( + + ) + }} + /> +
+
+ +
+
+ +
+
+ +
+
+ ); + } else { + moreInfo = ( + + + + ); + } + + activityList[i] = ( +
+
+
{devicePlatform}
+
+
+ + ), + time: ( + + ) + }} + /> +
+ {moreInfo} +
+
+
+ +
+
+ ); + } + + let content; + if (this.state.sessions.loading) { + content = ; + } else { + content =
{activityList}
; + } + + return ( + + + + + + + +

+ +

+ {content} +
+
+ ); + } +} diff --git a/webapp/components/activity_log_modal/index.js b/webapp/components/activity_log_modal/index.js new file mode 100644 index 000000000..1c4890c65 --- /dev/null +++ b/webapp/components/activity_log_modal/index.js @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {revokeSession, getSessions} from 'mattermost-redux/actions/users'; + +import ActivityLogModal from './activity_log_modal.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getSessions, + revokeSession + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ActivityLogModal); diff --git a/webapp/components/add_users_to_team.jsx b/webapp/components/add_users_to_team.jsx deleted file mode 100644 index f0936c0d7..000000000 --- a/webapp/components/add_users_to_team.jsx +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import MultiSelect from 'components/multiselect/multiselect.jsx'; -import ProfilePicture from 'components/profile_picture.jsx'; - -import {addUsersToTeam} from 'actions/team_actions.jsx'; -import {searchUsersNotInTeam} from 'actions/user_actions.jsx'; - -import UserStore from 'stores/user_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import Constants from 'utils/constants.jsx'; -import {displayUsernameForUser} from 'utils/utils.jsx'; -import Client from 'client/web_client.jsx'; - -import React from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router/es6'; - -const USERS_PER_PAGE = 50; -const MAX_SELECTABLE_VALUES = 20; - -export default class AddUsersToTeam extends React.Component { - constructor(props) { - super(props); - - this.handleHide = this.handleHide.bind(this); - this.handleExit = this.handleExit.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.onChange = this.onChange.bind(this); - this.search = this.search.bind(this); - this.addValue = this.addValue.bind(this); - - this.searchTimeoutId = 0; - - this.state = { - users: null, - values: [], - show: true, - search: false - }; - } - - componentDidMount() { - UserStore.addChangeListener(this.onChange); - UserStore.addNotInTeamChangeListener(this.onChange); - UserStore.addStatusesChangeListener(this.onChange); - - AsyncClient.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); - } - - componentWillUnmount() { - UserStore.removeChangeListener(this.onChange); - UserStore.removeNotInTeamChangeListener(this.onChange); - UserStore.removeStatusesChangeListener(this.onChange); - } - - handleHide() { - this.setState({show: false}); - } - - handleExit() { - if (this.exitToChannel) { - browserHistory.push(this.exitToChannel); - } - - if (this.props.onModalDismissed) { - this.props.onModalDismissed(); - } - } - - handleSubmit(e) { - if (e) { - e.preventDefault(); - } - - const userIds = this.state.values.map((v) => v.id); - if (userIds.length === 0) { - return; - } - - addUsersToTeam(TeamStore.getCurrentId(), userIds); - - this.handleHide(); - } - - addValue(value) { - const values = Object.assign([], this.state.values); - if (values.indexOf(value) === -1) { - values.push(value); - } - - this.setState({values}); - } - - onChange(force) { - if (this.state.search && !force) { - return; - } - - const users = Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)); - - for (let i = 0; i < users.length; i++) { - const user = Object.assign({}, users[i]); - user.value = user.id; - user.label = '@' + user.username; - users[i] = user; - } - - this.setState({ - users - }); - } - - handlePageChange(page, prevPage) { - if (page > prevPage) { - AsyncClient.getProfilesNotInTeam((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); - } - } - - search(term) { - clearTimeout(this.searchTimeoutId); - - if (term === '') { - this.onChange(true); - this.setState({search: false}); - this.searchTimeoutId = ''; - return; - } - - const teamId = TeamStore.getCurrentId(); - - const searchTimeoutId = setTimeout( - () => { - searchUsersNotInTeam( - term, - teamId, - {}, - (users) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - - let indexToDelete = -1; - for (let i = 0; i < users.length; i++) { - if (users[i].id === UserStore.getCurrentId()) { - indexToDelete = i; - } - users[i].value = users[i].id; - users[i].label = '@' + users[i].username; - } - - if (indexToDelete !== -1) { - users.splice(indexToDelete, 1); - } - this.setState({search: true, users}); - } - ); - }, - Constants.SEARCH_TIMEOUT_MILLISECONDS - ); - - this.searchTimeoutId = searchTimeoutId; - } - - handleDelete(values) { - this.setState({values}); - } - - renderOption(option, isSelected, onAdd) { - var rowSelected = ''; - if (isSelected) { - rowSelected = 'more-modal__row--selected'; - } - - return ( -
onAdd(option)} - > - -
-
- {displayUsernameForUser(option)} -
-
- {option.email} -
-
-
-
- -
-
-
- ); - } - - renderValue(user) { - return user.username; - } - - render() { - const numRemainingText = ( - - ); - - const buttonSubmitText = ( - - ); - - return ( - - - - {TeamStore.getCurrent().display_name} - ) - }} - /> - - - - - - - ); - } -} - -AddUsersToTeam.propTypes = { - onModalDismissed: React.PropTypes.func -}; diff --git a/webapp/components/add_users_to_team/add_users_to_team.jsx b/webapp/components/add_users_to_team/add_users_to_team.jsx new file mode 100644 index 000000000..ae6fd8c4e --- /dev/null +++ b/webapp/components/add_users_to_team/add_users_to_team.jsx @@ -0,0 +1,257 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import MultiSelect from 'components/multiselect/multiselect.jsx'; +import ProfilePicture from 'components/profile_picture.jsx'; + +import {addUsersToTeam} from 'actions/team_actions.jsx'; +import {searchUsersNotInTeam} from 'actions/user_actions.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import Constants from 'utils/constants.jsx'; +import {displayUsernameForUser} from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +import store from 'stores/redux_store.jsx'; +import {searchProfilesNotInCurrentTeam} from 'mattermost-redux/selectors/entities/users'; + +const USERS_PER_PAGE = 50; +const MAX_SELECTABLE_VALUES = 20; + +export default class AddUsersToTeam extends React.Component { + static propTypes = { + onModalDismissed: React.PropTypes.func, + actions: React.PropTypes.shape({ + getProfilesNotInTeam: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleExit = this.handleExit.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.onChange = this.onChange.bind(this); + this.search = this.search.bind(this); + this.addValue = this.addValue.bind(this); + this.handlePageChange = this.handlePageChange.bind(this); + + this.searchTimeoutId = 0; + + this.state = { + users: Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)), + values: [], + show: true, + search: false + }; + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); + UserStore.addNotInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + + this.props.actions.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + UserStore.removeNotInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + } + + handleHide() { + this.setState({show: false}); + } + + handleExit() { + if (this.exitToChannel) { + browserHistory.push(this.exitToChannel); + } + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSubmit(e) { + if (e) { + e.preventDefault(); + } + + const userIds = this.state.values.map((v) => v.id); + if (userIds.length === 0) { + return; + } + + addUsersToTeam(TeamStore.getCurrentId(), userIds); + + this.handleHide(); + } + + addValue(value) { + const values = Object.assign([], this.state.values); + if (values.indexOf(value) === -1) { + values.push(value); + } + + this.setState({values}); + } + + onChange() { + let users; + if (this.term) { + users = Object.assign([], searchProfilesNotInCurrentTeam(store.getState(), this.term, true)); + } else { + users = Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)); + } + + for (let i = 0; i < users.length; i++) { + const user = Object.assign({}, users[i]); + user.value = user.id; + user.label = '@' + user.username; + users[i] = user; + } + + this.setState({ + users + }); + } + + handlePageChange(page, prevPage) { + if (page > prevPage) { + this.props.actions.getProfilesNotInTeam(TeamStore.getCurrentId(), page + 1, USERS_PER_PAGE); + } + } + + search(term) { + clearTimeout(this.searchTimeoutId); + this.term = term; + + if (term === '') { + this.onChange(); + return; + } + + this.searchTimeoutId = setTimeout( + () => { + searchUsersNotInTeam(term, TeamStore.getCurrentId(), {}); + }, + Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + } + + handleDelete(values) { + this.setState({values}); + } + + renderOption(option, isSelected, onAdd) { + var rowSelected = ''; + if (isSelected) { + rowSelected = 'more-modal__row--selected'; + } + + return ( +
onAdd(option)} + > + +
+
+ {displayUsernameForUser(option)} +
+
+ {option.email} +
+
+
+
+ +
+
+
+ ); + } + + renderValue(user) { + return user.username; + } + + render() { + const numRemainingText = ( + + ); + + const buttonSubmitText = ( + + ); + + return ( + + + + {TeamStore.getCurrent().display_name} + ) + }} + /> + + + + + + + ); + } +} diff --git a/webapp/components/add_users_to_team/index.js b/webapp/components/add_users_to_team/index.js new file mode 100644 index 000000000..d38aeb4e5 --- /dev/null +++ b/webapp/components/add_users_to_team/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getProfilesNotInTeam} from 'mattermost-redux/actions/users'; + +import AddUsersToTeam from './add_users_to_team.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getProfilesNotInTeam + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddUsersToTeam); diff --git a/webapp/components/admin_console/system_users/system_users.jsx b/webapp/components/admin_console/system_users/system_users.jsx index 0b967dead..7bc4b81ed 100644 --- a/webapp/components/admin_console/system_users/system_users.jsx +++ b/webapp/components/admin_console/system_users/system_users.jsx @@ -23,6 +23,9 @@ import * as Utils from 'utils/utils.jsx'; import SystemUsersList from './system_users_list.jsx'; +import store from 'stores/redux_store.jsx'; +import {searchProfiles, searchProfilesInTeam} from 'mattermost-redux/selectors/entities/users'; + const ALL_USERS = ''; const NO_TEAM = 'no_team'; @@ -120,25 +123,18 @@ export default class SystemUsers extends React.Component { updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) { if (term) { - if (teamId === this.state.teamId) { - // Search results aren't in the store, so manually update the users in them - const users = [...this.state.users]; - - for (let i = 0; i < users.length; i++) { - const user = users[i]; - - if (UserStore.hasProfile(user.id)) { - users[i] = UserStore.getProfile(user.id); - } - } - - this.setState({ - users - }); + let users; + if (teamId) { + users = searchProfilesInTeam(store.getState(), teamId, term); } else { - this.doSearch(teamId, term, true); + users = searchProfiles(store.getState(), term); } + if (users.length === 0 && UserStore.hasProfile(term)) { + users = [UserStore.getProfile(term)]; + } + + this.setState({users}); return; } @@ -179,11 +175,11 @@ export default class SystemUsers extends React.Component { // Paging isn't supported while searching if (this.state.teamId === ALL_USERS) { - loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete); + loadProfiles(page, USERS_PER_PAGE, this.loadComplete); } else if (this.state.teamId === NO_TEAM) { loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete); } else { - loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete); + loadProfilesAndTeamMembers(page + 1, USERS_PER_PAGE, this.state.teamId, this.loadComplete); } } @@ -204,11 +200,9 @@ export default class SystemUsers extends React.Component { doSearch(teamId, term, now = false) { clearTimeout(this.searchTimeoutId); + this.term = term; - this.setState({ - loading: true, - users: [] - }); + this.setState({loading: true}); const options = { [UserSearchOptions.ALLOW_INACTIVE]: true @@ -217,74 +211,45 @@ export default class SystemUsers extends React.Component { options[UserSearchOptions.WITHOUT_TEAM] = true; } - const searchTimeoutId = setTimeout( + this.searchTimeoutId = setTimeout( () => { searchUsers( term, teamId, options, (users) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - - if (users.length > 0) { - this.setState({ - loading: false, - users - }); - } else if (term.length === USER_ID_LENGTH) { + if (users.length === 0 && term.length === USER_ID_LENGTH) { // This term didn't match any users name, but it does look like it might be a user's ID - this.getUserById(term, searchTimeoutId); + this.getUserById(term); } else { - this.setState({ - loading: false - }); + this.setState({loading: false}); } }, () => { - this.setState({ - loading: false - }); + this.setState({loading: false}); } ); }, now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS ); - - this.searchTimeoutId = searchTimeoutId; } - getUserById(id, searchTimeoutId) { + getUserById(id) { if (UserStore.hasProfile(id)) { - this.setState({ - loading: false, - users: [UserStore.getProfile(id)] - }); - + this.setState({loading: false}); return; } getUser( id, - (user) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - + () => { this.setState({ - loading: false, - users: [user] + loading: false }); }, () => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - this.setState({ - loading: false, - users: [] + loading: false }); } ); diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 5ebe1b745..82864d48c 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -5,11 +5,11 @@ import $ from 'jquery'; import 'bootstrap'; import NavbarSearchBox from './search_bar.jsx'; import MessageWrapper from './message_wrapper.jsx'; -import PopoverListMembers from './popover_list_members.jsx'; +import PopoverListMembers from 'components/popover_list_members'; import EditChannelHeaderModal from './edit_channel_header_modal.jsx'; import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; import ChannelInfoModal from './channel_info_modal.jsx'; -import ChannelInviteModal from './channel_invite_modal.jsx'; +import ChannelInviteModal from 'components/channel_invite_modal'; import ChannelMembersModal from './channel_members_modal.jsx'; import ChannelNotificationsModal from './channel_notifications_modal.jsx'; import DeleteChannelModal from './delete_channel_modal.jsx'; diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx deleted file mode 100644 index d41948a2b..000000000 --- a/webapp/components/channel_invite_modal.jsx +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelInviteButton from './channel_invite_button.jsx'; -import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; -import LoadingScreen from './loading_screen.jsx'; - -import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; - -import {searchUsers} from 'actions/user_actions.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import * as UserAgent from 'utils/user_agent.jsx'; -import Constants from 'utils/constants.jsx'; - -import React from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; - -const USERS_PER_PAGE = 50; - -export default class ChannelInviteModal extends React.Component { - constructor(props) { - super(props); - - this.onChange = this.onChange.bind(this); - this.onStatusChange = this.onStatusChange.bind(this); - this.onHide = this.onHide.bind(this); - this.handleInviteError = this.handleInviteError.bind(this); - this.nextPage = this.nextPage.bind(this); - this.search = this.search.bind(this); - - this.term = ''; - this.searchTimeoutId = 0; - - const channelStats = ChannelStore.getStats(props.channel.id); - const teamStats = TeamStore.getCurrentStats(); - - this.state = { - users: null, - total: teamStats.active_member_count - channelStats.member_count, - show: true, - search: false, - statusChange: false - }; - } - - componentDidMount() { - TeamStore.addStatsChangeListener(this.onChange); - ChannelStore.addStatsChangeListener(this.onChange); - UserStore.addNotInChannelChangeListener(this.onChange); - UserStore.addStatusesChangeListener(this.onStatusChange); - - AsyncClient.getProfilesNotInChannel(this.props.channel.id, 0); - AsyncClient.getTeamStats(TeamStore.getCurrentId()); - } - - componentWillUnmount() { - TeamStore.removeStatsChangeListener(this.onChange); - ChannelStore.removeStatsChangeListener(this.onChange); - UserStore.removeNotInChannelChangeListener(this.onChange); - UserStore.removeStatusesChangeListener(this.onStatusChange); - } - - onChange(force) { - if (this.state.search && !force) { - this.search(this.term); - return; - } - - const channelStats = ChannelStore.getStats(this.props.channel.id); - const teamStats = TeamStore.getCurrentStats(); - - this.setState({ - users: UserStore.getProfileListNotInChannel(this.props.channel.id, true), - total: teamStats.active_member_count - channelStats.member_count - }); - } - - onStatusChange() { - // Initiate a render to pick up on new statuses - this.setState({ - statusChange: !this.state.statusChange - }); - } - - onHide() { - this.setState({show: false}); - } - - handleInviteError(err) { - if (err) { - this.setState({ - inviteError: err.message - }); - } else { - this.setState({ - inviteError: null - }); - } - } - - nextPage(page) { - AsyncClient.getProfilesNotInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); - } - - search(term) { - clearTimeout(this.searchTimeoutId); - - this.term = term; - - if (term === '') { - this.onChange(true); - this.setState({search: false}); - this.searchTimeoutId = ''; - return; - } - - const searchTimeoutId = setTimeout( - () => { - searchUsers( - term, - TeamStore.getCurrentId(), - {not_in_channel_id: this.props.channel.id}, - (users) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - - this.setState({search: true, users}); - } - ); - }, - Constants.SEARCH_TIMEOUT_MILLISECONDS - ); - - this.searchTimeoutId = searchTimeoutId; - } - - render() { - let inviteError = null; - if (this.state.inviteError) { - inviteError = (); - } - - let content; - if (this.state.loading) { - content = (); - } else { - content = ( - - ); - } - - return ( - - - - - {this.props.channel.display_name} - - - - {inviteError} - {content} - - - ); - } -} - -ChannelInviteModal.propTypes = { - onHide: React.PropTypes.func.isRequired, - channel: React.PropTypes.object.isRequired -}; diff --git a/webapp/components/channel_invite_modal/channel_invite_modal.jsx b/webapp/components/channel_invite_modal/channel_invite_modal.jsx new file mode 100644 index 000000000..847af16f6 --- /dev/null +++ b/webapp/components/channel_invite_modal/channel_invite_modal.jsx @@ -0,0 +1,188 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ChannelInviteButton from 'components/channel_invite_button.jsx'; +import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {searchUsers} from 'actions/user_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; +import Constants from 'utils/constants.jsx'; + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import store from 'stores/redux_store.jsx'; +import {searchProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; + +const USERS_PER_PAGE = 50; + +export default class ChannelInviteModal extends React.Component { + static propTypes = { + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired, + actions: React.PropTypes.shape({ + getProfilesNotInChannel: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.onStatusChange = this.onStatusChange.bind(this); + this.onHide = this.onHide.bind(this); + this.handleInviteError = this.handleInviteError.bind(this); + this.nextPage = this.nextPage.bind(this); + this.search = this.search.bind(this); + + this.term = ''; + this.searchTimeoutId = 0; + + const channelStats = ChannelStore.getStats(props.channel.id); + const teamStats = TeamStore.getCurrentStats(); + + this.state = { + users: UserStore.getProfileListNotInChannel(props.channel.id, true), + total: teamStats.active_member_count - channelStats.member_count, + show: true, + statusChange: false + }; + } + + componentDidMount() { + TeamStore.addStatsChangeListener(this.onChange); + ChannelStore.addStatsChangeListener(this.onChange); + UserStore.addNotInChannelChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onStatusChange); + + this.props.actions.getProfilesNotInChannel(TeamStore.getCurrentId(), this.props.channel.id, 0); + AsyncClient.getTeamStats(TeamStore.getCurrentId()); + } + + componentWillUnmount() { + TeamStore.removeStatsChangeListener(this.onChange); + ChannelStore.removeStatsChangeListener(this.onChange); + UserStore.removeNotInChannelChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onStatusChange); + } + + onChange() { + let users; + if (this.term) { + users = searchProfilesNotInCurrentChannel(store.getState(), this.term, true); + } else { + users = UserStore.getProfileListNotInChannel(this.props.channel.id, true); + } + + const channelStats = ChannelStore.getStats(this.props.channel.id); + const teamStats = TeamStore.getCurrentStats(); + + this.setState({ + users, + total: teamStats.active_member_count - channelStats.member_count + }); + } + + onStatusChange() { + // Initiate a render to pick up on new statuses + this.setState({ + statusChange: !this.state.statusChange + }); + } + + onHide() { + this.setState({show: false}); + } + + handleInviteError(err) { + if (err) { + this.setState({ + inviteError: err.message + }); + } else { + this.setState({ + inviteError: null + }); + } + } + + nextPage(page) { + this.props.actions.getProfilesNotInChannel(TeamStore.getCurrentId(), this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + + search(term) { + clearTimeout(this.searchTimeoutId); + this.term = term; + + if (term === '') { + this.onChange(); + return; + } + + this.searchTimeoutId = setTimeout( + () => { + searchUsers(term, TeamStore.getCurrentId(), {not_in_channel_id: this.props.channel.id}); + }, + Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + } + + render() { + let inviteError = null; + if (this.state.inviteError) { + inviteError = (); + } + + let content; + if (this.state.loading) { + content = (); + } else { + content = ( + + ); + } + + return ( + + + + + {this.props.channel.display_name} + + + + {inviteError} + {content} + + + ); + } +} diff --git a/webapp/components/channel_invite_modal/index.js b/webapp/components/channel_invite_modal/index.js new file mode 100644 index 000000000..c8bdb54f5 --- /dev/null +++ b/webapp/components/channel_invite_modal/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getProfilesNotInChannel} from 'mattermost-redux/actions/users'; + +import ChannelInviteModal from './channel_invite_modal.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getProfilesNotInChannel + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChannelInviteModal); diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index 482135a5e..34fdc536c 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -209,19 +209,15 @@ export default class LoginController extends React.Component { } finishSignin(team) { - GlobalActions.emitInitialLoad( - () => { - const query = this.props.location.query; - GlobalActions.loadDefaultLocale(); - if (query.redirect_to) { - browserHistory.push(query.redirect_to); - } else if (team) { - browserHistory.push(`/${team.name}`); - } else { - GlobalActions.redirectUserToDefaultTeam(); - } - } - ); + const query = this.props.location.query; + GlobalActions.loadDefaultLocale(); + if (query.redirect_to) { + browserHistory.push(query.redirect_to); + } else if (team) { + browserHistory.push(`/${team.name}`); + } else { + GlobalActions.redirectUserToDefaultTeam(); + } } handleLoginIdChange(e) { diff --git a/webapp/components/member_list_channel.jsx b/webapp/components/member_list_channel.jsx index e9eef9fb8..df000c132 100644 --- a/webapp/components/member_list_channel.jsx +++ b/webapp/components/member_list_channel.jsx @@ -17,6 +17,9 @@ import * as UserAgent from 'utils/user_agent.jsx'; import React from 'react'; +import store from 'stores/redux_store.jsx'; +import {searchProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; + const USERS_PER_PAGE = 50; export default class MemberListChannel extends React.Component { @@ -29,6 +32,7 @@ export default class MemberListChannel extends React.Component { this.loadComplete = this.loadComplete.bind(this); this.searchTimeoutId = 0; + this.term = ''; const stats = ChannelStore.getCurrentStats(); @@ -37,8 +41,6 @@ export default class MemberListChannel extends React.Component { teamMembers: Object.assign({}, TeamStore.getMembersInTeam()), channelMembers: Object.assign({}, ChannelStore.getMembersInChannel()), total: stats.member_count, - search: false, - term: '', loading: true }; } @@ -66,16 +68,16 @@ export default class MemberListChannel extends React.Component { this.setState({loading: false}); } - onChange(force) { - if (this.state.search && !force) { - return; - } else if (this.state.search) { - this.search(this.state.term); - return; + onChange() { + let users; + if (this.term) { + users = searchProfilesInCurrentChannel(store.getState(), this.term); + } else { + users = UserStore.getProfileListInChannel(); } this.setState({ - users: UserStore.getProfileListInChannel(), + users, teamMembers: Object.assign({}, TeamStore.getMembersInTeam()), channelMembers: Object.assign({}, ChannelStore.getMembersInChannel()) }); @@ -87,43 +89,30 @@ export default class MemberListChannel extends React.Component { } nextPage(page) { - loadProfilesAndTeamMembersAndChannelMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + loadProfilesAndTeamMembersAndChannelMembers(page + 1, USERS_PER_PAGE); } search(term) { clearTimeout(this.searchTimeoutId); + this.term = term; if (term === '') { - this.setState({ - search: false, - term, - users: UserStore.getProfileListInChannel(), - teamMembers: Object.assign([], TeamStore.getMembersInTeam()), - channelMembers: Object.assign([], ChannelStore.getMembersInChannel()) - }); + this.setState({loading: false}); this.searchTimeoutId = ''; + this.onChange(); return; } const searchTimeoutId = setTimeout( () => { - searchUsers( - term, - TeamStore.getCurrentId(), - {}, + searchUsers(term, TeamStore.getCurrentId(), {}, (users) => { if (searchTimeoutId !== this.searchTimeoutId) { return; } - this.setState({ - loading: true, - search: true, - users, - term, - teamMembers: Object.assign([], TeamStore.getMembersInTeam()), - channelMembers: Object.assign([], ChannelStore.getMembersInChannel()) - }); + this.setState({loading: true}); + loadTeamMembersAndChannelMembersForProfilesList(users, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete); } ); diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx index 0aa1e6e57..212536dc8 100644 --- a/webapp/components/member_list_team.jsx +++ b/webapp/components/member_list_team.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; -import TeamMembersDropdown from 'components/team_members_dropdown.jsx'; +import TeamMembersDropdown from 'components/team_members_dropdown'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -16,6 +16,9 @@ import * as UserAgent from 'utils/user_agent.jsx'; import React from 'react'; +import store from 'stores/redux_store.jsx'; +import {searchProfilesInCurrentTeam} from 'mattermost-redux/selectors/entities/users'; + const USERS_PER_PAGE = 50; export default class MemberListTeam extends React.Component { @@ -29,6 +32,7 @@ export default class MemberListTeam extends React.Component { this.loadComplete = this.loadComplete.bind(this); this.searchTimeoutId = 0; + this.term = ''; const stats = TeamStore.getCurrentStats(); @@ -36,8 +40,6 @@ export default class MemberListTeam extends React.Component { users: UserStore.getProfileListInTeam(), teamMembers: Object.assign([], TeamStore.getMembersInTeam()), total: stats.total_member_count, - search: false, - term: '', loading: true }; } @@ -67,15 +69,15 @@ export default class MemberListTeam extends React.Component { this.onChange(true); } - onChange(force) { - if (this.state.search && !force) { - return; - } else if (this.state.search) { - this.search(this.state.term); - return; + onChange() { + let users; + if (this.term) { + users = searchProfilesInCurrentTeam(store.getState(), this.term); + } else { + users = UserStore.getProfileListInTeam(); } - this.setState({users: UserStore.getProfileListInTeam(), teamMembers: Object.assign([], TeamStore.getMembersInTeam())}); + this.setState({users, teamMembers: Object.assign([], TeamStore.getMembersInTeam())}); } onStatsChange() { @@ -84,15 +86,17 @@ export default class MemberListTeam extends React.Component { } nextPage(page) { - loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + loadProfilesAndTeamMembers(page, USERS_PER_PAGE); } search(term) { clearTimeout(this.searchTimeoutId); + this.term = term; if (term === '') { - this.setState({search: false, term, users: UserStore.getProfileListInTeam(), teamMembers: Object.assign([], TeamStore.getMembersInTeam())}); + this.setState({loading: false}); this.searchTimeoutId = ''; + this.onChange(); return; } @@ -106,7 +110,7 @@ export default class MemberListTeam extends React.Component { if (searchTimeoutId !== this.searchTimeoutId) { return; } - this.setState({loading: true, search: true, users, term, teamMembers: Object.assign([], TeamStore.getMembersInTeam())}); + this.setState({loading: true}); loadTeamMembersForProfilesList(users, TeamStore.getCurrentId(), this.loadComplete); } ); diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx deleted file mode 100644 index c8a0d22ee..000000000 --- a/webapp/components/more_direct_channels.jsx +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import MultiSelect from 'components/multiselect/multiselect.jsx'; -import ProfilePicture from 'components/profile_picture.jsx'; - -import {searchUsers} from 'actions/user_actions.jsx'; -import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx'; - -import UserStore from 'stores/user_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import Constants from 'utils/constants.jsx'; -import {displayUsernameForUser} from 'utils/utils.jsx'; -import Client from 'client/web_client.jsx'; - -import React from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router/es6'; - -const USERS_PER_PAGE = 50; -const MAX_SELECTABLE_VALUES = Constants.MAX_USERS_IN_GM - 1; - -export default class MoreDirectChannels extends React.Component { - constructor(props) { - super(props); - - this.handleHide = this.handleHide.bind(this); - this.handleExit = this.handleExit.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.onChange = this.onChange.bind(this); - this.search = this.search.bind(this); - this.addValue = this.addValue.bind(this); - - this.searchTimeoutId = 0; - this.listType = global.window.mm_config.RestrictDirectMessage; - - const values = []; - if (props.startingUsers) { - for (let i = 0; i < props.startingUsers.length; i++) { - const user = Object.assign({}, props.startingUsers[i]); - user.value = user.id; - user.label = '@' + user.username; - values.push(user); - } - } - - this.state = { - users: null, - values, - show: true, - search: false, - loadingChannel: -1 - }; - } - - componentDidMount() { - UserStore.addChangeListener(this.onChange); - UserStore.addInTeamChangeListener(this.onChange); - UserStore.addStatusesChangeListener(this.onChange); - - if (this.listType === 'any') { - AsyncClient.getProfiles(0, USERS_PER_PAGE * 2); - } else { - AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); - } - } - - componentWillUnmount() { - UserStore.removeChangeListener(this.onChange); - UserStore.removeInTeamChangeListener(this.onChange); - UserStore.removeStatusesChangeListener(this.onChange); - } - - handleHide() { - this.setState({show: false}); - } - - handleExit() { - if (this.exitToChannel) { - browserHistory.push(this.exitToChannel); - } - - if (this.props.onModalDismissed) { - this.props.onModalDismissed(); - } - } - - handleSubmit(e) { - if (e) { - e.preventDefault(); - } - - if (this.state.loadingChannel !== -1) { - return; - } - - const userIds = this.state.values.map((v) => v.id); - if (userIds.length === 0) { - return; - } - - this.setState({loadingChannel: 1}); - - const success = (channel) => { - // Due to how react-overlays Modal handles focus, we delay pushing - // the new channel information until the modal is fully exited. - // The channel information will be pushed in `handleExit` - this.exitToChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name; - this.setState({loadingChannel: -1}); - this.handleHide(); - }; - - const error = () => { - this.setState({loadingChannel: -1}); - }; - - if (userIds.length === 1) { - openDirectChannelToUser(userIds[0], success, error); - } else { - openGroupChannelToUsers(userIds, success, error); - } - } - - addValue(value) { - const values = Object.assign([], this.state.values); - if (values.indexOf(value) === -1) { - values.push(value); - } - - this.setState({values}); - } - - onChange(force) { - if (this.state.search && !force) { - return; - } - - let users; - if (this.listType === 'any') { - users = Object.assign([], UserStore.getProfileList(true)); - } else { - users = Object.assign([], UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true)); - } - - for (let i = 0; i < users.length; i++) { - const user = Object.assign({}, users[i]); - user.value = user.id; - user.label = '@' + user.username; - users[i] = user; - } - - this.setState({ - users - }); - } - - handlePageChange(page, prevPage) { - if (page > prevPage) { - AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); - } - } - - search(term) { - clearTimeout(this.searchTimeoutId); - - if (term === '') { - this.onChange(true); - this.setState({search: false}); - this.searchTimeoutId = ''; - return; - } - - let teamId; - if (this.listType === 'any') { - teamId = ''; - } else { - teamId = TeamStore.getCurrentId(); - } - - const searchTimeoutId = setTimeout( - () => { - searchUsers( - term, - teamId, - {}, - (users) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - - let indexToDelete = -1; - for (let i = 0; i < users.length; i++) { - if (users[i].id === UserStore.getCurrentId()) { - indexToDelete = i; - } - users[i].value = users[i].id; - users[i].label = '@' + users[i].username; - } - - if (indexToDelete !== -1) { - users.splice(indexToDelete, 1); - } - this.setState({search: true, users}); - } - ); - }, - Constants.SEARCH_TIMEOUT_MILLISECONDS - ); - - this.searchTimeoutId = searchTimeoutId; - } - - handleDelete(values) { - this.setState({values}); - } - - renderOption(option, isSelected, onAdd) { - var rowSelected = ''; - if (isSelected) { - rowSelected = 'more-modal__row--selected'; - } - - return ( -
onAdd(option)} - > - -
-
- {displayUsernameForUser(option)} -
-
- {option.email} -
-
-
-
- -
-
-
- ); - } - - renderValue(user) { - return user.username; - } - - render() { - let note; - if (this.props.startingUsers) { - if (this.state.values && this.state.values.length >= MAX_SELECTABLE_VALUES) { - note = ( - - ); - } else { - note = ( - - ); - } - } - - const buttonSubmitText = ( - - ); - - const numRemainingText = ( - - ); - - return ( - - - - - - - - - - - ); - } -} - -MoreDirectChannels.propTypes = { - startingUsers: React.PropTypes.arrayOf(React.PropTypes.object), - onModalDismissed: React.PropTypes.func -}; diff --git a/webapp/components/more_direct_channels/index.js b/webapp/components/more_direct_channels/index.js new file mode 100644 index 000000000..a56f45886 --- /dev/null +++ b/webapp/components/more_direct_channels/index.js @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getProfiles, getProfilesInTeam} from 'mattermost-redux/actions/users'; + +import MoreDirectChannels from './more_direct_channels.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getProfiles, + getProfilesInTeam + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MoreDirectChannels); diff --git a/webapp/components/more_direct_channels/more_direct_channels.jsx b/webapp/components/more_direct_channels/more_direct_channels.jsx new file mode 100644 index 000000000..50e2c4e48 --- /dev/null +++ b/webapp/components/more_direct_channels/more_direct_channels.jsx @@ -0,0 +1,328 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import MultiSelect from 'components/multiselect/multiselect.jsx'; +import ProfilePicture from 'components/profile_picture.jsx'; + +import {searchUsers} from 'actions/user_actions.jsx'; +import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import Constants from 'utils/constants.jsx'; +import {displayUsernameForUser} from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +import store from 'stores/redux_store.jsx'; +import {searchProfiles, searchProfilesInCurrentTeam} from 'mattermost-redux/selectors/entities/users'; + +const USERS_PER_PAGE = 50; +const MAX_SELECTABLE_VALUES = Constants.MAX_USERS_IN_GM - 1; + +export default class MoreDirectChannels extends React.Component { + static propTypes = { + startingUsers: React.PropTypes.arrayOf(React.PropTypes.object), + onModalDismissed: React.PropTypes.func, + actions: React.PropTypes.shape({ + getProfiles: React.PropTypes.func.isRequired, + getProfilesInTeam: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleExit = this.handleExit.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.onChange = this.onChange.bind(this); + this.search = this.search.bind(this); + this.addValue = this.addValue.bind(this); + + this.searchTimeoutId = 0; + this.term = ''; + this.listType = global.window.mm_config.RestrictDirectMessage; + + const values = []; + if (props.startingUsers) { + for (let i = 0; i < props.startingUsers.length; i++) { + const user = Object.assign({}, props.startingUsers[i]); + user.value = user.id; + user.label = '@' + user.username; + values.push(user); + } + } + + this.state = { + users: null, + values, + show: true, + search: false, + loadingChannel: -1 + }; + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); + UserStore.addInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + + if (this.listType === 'any') { + this.props.actions.getProfiles(0, USERS_PER_PAGE * 2); + } else { + this.props.actions.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); + } + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + UserStore.removeInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + } + + handleHide() { + this.setState({show: false}); + } + + handleExit() { + if (this.exitToChannel) { + browserHistory.push(this.exitToChannel); + } + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSubmit(e) { + if (e) { + e.preventDefault(); + } + + if (this.state.loadingChannel !== -1) { + return; + } + + const userIds = this.state.values.map((v) => v.id); + if (userIds.length === 0) { + return; + } + + this.setState({loadingChannel: 1}); + + const success = (channel) => { + // Due to how react-overlays Modal handles focus, we delay pushing + // the new channel information until the modal is fully exited. + // The channel information will be pushed in `handleExit` + this.exitToChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name; + this.setState({loadingChannel: -1}); + this.handleHide(); + }; + + const error = () => { + this.setState({loadingChannel: -1}); + }; + + if (userIds.length === 1) { + openDirectChannelToUser(userIds[0], success, error); + } else { + openGroupChannelToUsers(userIds, success, error); + } + } + + addValue(value) { + const values = Object.assign([], this.state.values); + if (values.indexOf(value) === -1) { + values.push(value); + } + + this.setState({values}); + } + + onChange() { + let users; + if (this.term) { + if (this.listType === 'any') { + users = Object.assign([], searchProfiles(store.getState(), this.term, true)); + } else { + users = Object.assign([], searchProfilesInCurrentTeam(store.getState(), this.term, true)); + } + } else if (this.listType === 'any') { + users = Object.assign([], UserStore.getProfileList(true)); + } else { + users = Object.assign([], UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true)); + } + + for (let i = 0; i < users.length; i++) { + const user = Object.assign({}, users[i]); + user.value = user.id; + user.label = '@' + user.username; + users[i] = user; + } + + this.setState({ + users + }); + } + + handlePageChange(page, prevPage) { + if (page > prevPage) { + if (this.listType === 'any') { + this.props.actions.getProfiles(page + 1, USERS_PER_PAGE); + } else { + this.props.actions.getProfilesInTeam(page + 1, USERS_PER_PAGE); + } + } + } + + search(term) { + clearTimeout(this.searchTimeoutId); + this.term = term; + + if (term === '') { + this.onChange(); + return; + } + + let teamId; + if (this.listType === 'any') { + teamId = ''; + } else { + teamId = TeamStore.getCurrentId(); + } + + this.searchTimeoutId = setTimeout( + () => { + searchUsers(term, teamId); + }, + Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + } + + handleDelete(values) { + this.setState({values}); + } + + renderOption(option, isSelected, onAdd) { + var rowSelected = ''; + if (isSelected) { + rowSelected = 'more-modal__row--selected'; + } + + return ( +
onAdd(option)} + > + +
+
+ {displayUsernameForUser(option)} +
+
+ {option.email} +
+
+
+
+ +
+
+
+ ); + } + + renderValue(user) { + return user.username; + } + + render() { + let note; + if (this.props.startingUsers) { + if (this.state.values && this.state.values.length >= MAX_SELECTABLE_VALUES) { + note = ( + + ); + } else { + note = ( + + ); + } + } + + const buttonSubmitText = ( + + ); + + const numRemainingText = ( + + ); + + return ( + + + + + + + + + + + ); + } +} diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index 22d2b8ae4..33df0b423 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -7,7 +7,7 @@ import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; import MessageWrapper from './message_wrapper.jsx'; import NotifyCounts from './notify_counts.jsx'; import ChannelInfoModal from './channel_info_modal.jsx'; -import ChannelInviteModal from './channel_invite_modal.jsx'; +import ChannelInviteModal from 'components/channel_invite_modal'; import ChannelMembersModal from './channel_members_modal.jsx'; import ChannelNotificationsModal from './channel_notifications_modal.jsx'; import DeleteChannelModal from './delete_channel_modal.jsx'; diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx deleted file mode 100644 index 3af53cb70..000000000 --- a/webapp/components/popover_list_members.jsx +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ProfilePicture from 'components/profile_picture.jsx'; - -import TeamStore from 'stores/team_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; - -import TeamMembersModal from './team_members_modal.jsx'; -import ChannelMembersModal from './channel_members_modal.jsx'; -import ChannelInviteModal from './channel_invite_modal.jsx'; - -import {openDirectChannelToUser} from 'actions/channel_actions.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import Client from 'client/web_client.jsx'; -import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; -import {canManageMembers} from 'utils/channel_utils.jsx'; - -import $ from 'jquery'; -import React from 'react'; -import {Popover, Overlay} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router/es6'; - -export default class PopoverListMembers extends React.Component { - constructor(props) { - super(props); - - this.showMembersModal = this.showMembersModal.bind(this); - - this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); - this.closePopover = this.closePopover.bind(this); - - this.state = { - showPopover: false, - showTeamMembersModal: false, - showChannelMembersModal: false, - showChannelInviteModal: false - }; - } - - componentDidUpdate() { - $('.member-list__popover .popover-content').perfectScrollbar(); - } - - handleShowDirectChannel(teammate, e) { - e.preventDefault(); - - openDirectChannelToUser( - teammate.id, - (channel, channelAlreadyExisted) => { - browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name); - if (channelAlreadyExisted) { - this.closePopover(); - } - }, - () => { - this.closePopover(); - } - ); - } - - closePopover() { - this.setState({showPopover: false}); - } - - showMembersModal(e) { - e.preventDefault(); - - this.setState({ - showPopover: false, - showChannelMembersModal: true - }); - } - - render() { - const popoverHtml = []; - const members = this.props.members; - const teamMembers = UserStore.getProfilesUsernameMap(); - const currentUserId = UserStore.getCurrentId(); - - const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); - const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); - const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); - - if (members && teamMembers) { - members.sort((a, b) => { - const aName = Utils.displayUsername(a.id); - const bName = Utils.displayUsername(b.id); - - return aName.localeCompare(bName); - }); - - members.forEach((m, i) => { - let button = ''; - if (currentUserId !== m.id && this.props.channel.type !== Constants.DM_CHANNEl) { - button = ( - this.handleShowDirectChannel(m, e)} - > - - - ); - } - - let name = ''; - if (teamMembers[m.username]) { - name = Utils.displayUsername(teamMembers[m.username].id); - } - - if (name) { - popoverHtml.push( -
- -
-
- {name} -
-
-
- {button} -
-
- ); - } - }); - - if (this.props.channel.type !== Constants.GM_CHANNEL) { - let membersName = ( - - ); - - const manageMembers = canManageMembers(this.props.channel, isSystemAdmin, isTeamAdmin, isChannelAdmin); - const isDefaultChannel = ChannelStore.isDefault(this.props.channel); - - if ((manageMembers === false && isDefaultChannel === false) || isDefaultChannel) { - membersName = ( - - ); - } - - popoverHtml.push( -
- - ); - } - } - - const count = this.props.memberCount; - let countText = '-'; - if (count > 0) { - countText = count.toString(); - } - - const title = ( - - ); - - let channelMembersModal; - if (this.state.showChannelMembersModal) { - channelMembersModal = ( - this.setState({showChannelMembersModal: false})} - showInviteModal={() => this.setState({showChannelInviteModal: true})} - channel={this.props.channel} - /> - ); - } - - let teamMembersModal; - if (this.state.showTeamMembersModal) { - teamMembersModal = ( - this.setState({showTeamMembersModal: false})} - isAdmin={isTeamAdmin || isSystemAdmin} - /> - ); - } - - let channelInviteModal; - if (this.state.showChannelInviteModal) { - channelInviteModal = ( - this.setState({showChannelInviteModal: false})} - channel={this.props.channel} - /> - ); - } - - return ( -
-
{ - this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover}); - AsyncClient.getProfilesInChannel(this.props.channel.id, 0); - }} - > - {countText} -
- this.state.popoverTarget} - placement='bottom' - > - -
{popoverHtml}
-
-
- {channelMembersModal} - {teamMembersModal} - {channelInviteModal} -
- ); - } -} - -PopoverListMembers.propTypes = { - channel: React.PropTypes.object.isRequired, - members: React.PropTypes.array.isRequired, - memberCount: React.PropTypes.number -}; diff --git a/webapp/components/popover_list_members/index.js b/webapp/components/popover_list_members/index.js new file mode 100644 index 000000000..3e9087e0d --- /dev/null +++ b/webapp/components/popover_list_members/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getProfilesInChannel} from 'mattermost-redux/actions/users'; + +import PopoverListMembers from './popover_list_members.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getProfilesInChannel + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(PopoverListMembers); diff --git a/webapp/components/popover_list_members/popover_list_members.jsx b/webapp/components/popover_list_members/popover_list_members.jsx new file mode 100644 index 000000000..e435126ff --- /dev/null +++ b/webapp/components/popover_list_members/popover_list_members.jsx @@ -0,0 +1,282 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ProfilePicture from 'components/profile_picture.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import TeamMembersModal from 'components/team_members_modal.jsx'; +import ChannelMembersModal from 'components/channel_members_modal.jsx'; +import ChannelInviteModal from 'components/channel_invite_modal'; + +import {openDirectChannelToUser} from 'actions/channel_actions.jsx'; + +import Client from 'client/web_client.jsx'; +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; +import {canManageMembers} from 'utils/channel_utils.jsx'; + +import $ from 'jquery'; +import React from 'react'; +import {Popover, Overlay} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +export default class PopoverListMembers extends React.Component { + static propTypes = { + channel: React.PropTypes.object.isRequired, + members: React.PropTypes.array.isRequired, + memberCount: React.PropTypes.number, + actions: React.PropTypes.shape({ + getProfilesInChannel: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.showMembersModal = this.showMembersModal.bind(this); + + this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); + this.closePopover = this.closePopover.bind(this); + + this.state = { + showPopover: false, + showTeamMembersModal: false, + showChannelMembersModal: false, + showChannelInviteModal: false + }; + } + + componentDidUpdate() { + $('.member-list__popover .popover-content').perfectScrollbar(); + } + + handleShowDirectChannel(teammate, e) { + e.preventDefault(); + + openDirectChannelToUser( + teammate.id, + (channel, channelAlreadyExisted) => { + browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name); + if (channelAlreadyExisted) { + this.closePopover(); + } + }, + () => { + this.closePopover(); + } + ); + } + + closePopover() { + this.setState({showPopover: false}); + } + + showMembersModal(e) { + e.preventDefault(); + + this.setState({ + showPopover: false, + showChannelMembersModal: true + }); + } + + render() { + const popoverHtml = []; + const members = this.props.members; + const teamMembers = UserStore.getProfilesUsernameMap(); + const currentUserId = UserStore.getCurrentId(); + + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); + const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); + + if (members && teamMembers) { + members.sort((a, b) => { + const aName = Utils.displayUsername(a.id); + const bName = Utils.displayUsername(b.id); + + return aName.localeCompare(bName); + }); + + members.forEach((m, i) => { + let button = ''; + if (currentUserId !== m.id && this.props.channel.type !== Constants.DM_CHANNEl) { + button = ( + this.handleShowDirectChannel(m, e)} + > + + + ); + } + + let name = ''; + if (teamMembers[m.username]) { + name = Utils.displayUsername(teamMembers[m.username].id); + } + + if (name) { + popoverHtml.push( +
+ +
+
+ {name} +
+
+
+ {button} +
+
+ ); + } + }); + + if (this.props.channel.type !== Constants.GM_CHANNEL) { + let membersName = ( + + ); + + const manageMembers = canManageMembers(this.props.channel, isSystemAdmin, isTeamAdmin, isChannelAdmin); + const isDefaultChannel = ChannelStore.isDefault(this.props.channel); + + if ((manageMembers === false && isDefaultChannel === false) || isDefaultChannel) { + membersName = ( + + ); + } + + popoverHtml.push( +
+ + ); + } + } + + const count = this.props.memberCount; + let countText = '-'; + if (count > 0) { + countText = count.toString(); + } + + const title = ( + + ); + + let channelMembersModal; + if (this.state.showChannelMembersModal) { + channelMembersModal = ( + this.setState({showChannelMembersModal: false})} + showInviteModal={() => this.setState({showChannelInviteModal: true})} + channel={this.props.channel} + /> + ); + } + + let teamMembersModal; + if (this.state.showTeamMembersModal) { + teamMembersModal = ( + this.setState({showTeamMembersModal: false})} + isAdmin={isTeamAdmin || isSystemAdmin} + /> + ); + } + + let channelInviteModal; + if (this.state.showChannelInviteModal) { + channelInviteModal = ( + this.setState({showChannelInviteModal: false})} + channel={this.props.channel} + /> + ); + } + + return ( +
+
{ + this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover}); + this.props.actions.getProfilesInChannel(this.props.channel.id, 0); + }} + > + {countText} +
+ this.state.popoverTarget} + placement='bottom' + > + +
{popoverHtml}
+
+
+ {channelMembersModal} + {teamMembersModal} + {channelInviteModal} +
+ ); + } +} + diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx index b49b4d509..6907e84e4 100644 --- a/webapp/components/root.jsx +++ b/webapp/components/root.jsx @@ -136,6 +136,7 @@ export default class Root extends React.Component { ); } } + Root.defaultProps = { }; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 72dcac992..8667802cc 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -4,7 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import NewChannelFlow from './new_channel_flow.jsx'; -import MoreDirectChannels from './more_direct_channels.jsx'; +import MoreDirectChannels from 'components/more_direct_channels'; import MoreChannels from 'components/more_channels.jsx'; import SidebarHeader from './sidebar_header.jsx'; import UnreadChannelIndicator from './unread_channel_indicator.jsx'; diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index 728017b27..256019b64 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -14,7 +14,7 @@ import AboutBuildModal from './about_build_modal.jsx'; import SidebarHeaderDropdownButton from './sidebar_header_dropdown_button.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; -import AddUsersToTeam from './add_users_to_team.jsx'; +import AddUsersToTeam from 'components/add_users_to_team'; import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index 784f06eac..aac7d58cc 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -6,7 +6,7 @@ import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import AboutBuildModal from './about_build_modal.jsx'; -import AddUsersToTeam from './add_users_to_team.jsx'; +import AddUsersToTeam from 'components/add_users_to_team'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx index c33fb45e0..976b0648b 100644 --- a/webapp/components/signup/components/signup_email.jsx +++ b/webapp/components/signup/components/signup_email.jsx @@ -8,7 +8,7 @@ import {trackEvent} from 'actions/diagnostics_actions.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import {getInviteInfo} from 'actions/team_actions.jsx'; -import {loginById, createUserWithInvite} from 'actions/user_actions.jsx'; +import {loadMe, loginById, createUserWithInvite} from 'actions/user_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -108,7 +108,7 @@ export default class SignupEmail extends React.Component { } finishSignup() { - GlobalActions.emitInitialLoad( + loadMe( () => { const query = this.props.location.query; GlobalActions.loadDefaultLocale(); @@ -132,7 +132,7 @@ export default class SignupEmail extends React.Component { BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); } - GlobalActions.emitInitialLoad( + loadMe( () => { const query = this.props.location.query; if (query.redirect_to) { diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx index 80fac3ecc..a101c248f 100644 --- a/webapp/components/signup/components/signup_ldap.jsx +++ b/webapp/components/signup/components/signup_ldap.jsx @@ -5,7 +5,7 @@ import FormError from 'components/form_error.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {addUserToTeamFromInvite} from 'actions/team_actions.jsx'; -import {webLoginByLdap} from 'actions/user_actions.jsx'; +import {loadMe, webLoginByLdap} from 'actions/user_actions.jsx'; import {trackEvent} from 'actions/diagnostics_actions.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -97,7 +97,7 @@ export default class SignupLdap extends React.Component { } finishSignup() { - GlobalActions.emitInitialLoad( + loadMe( () => { const query = this.props.location.query; GlobalActions.loadDefaultLocale(); diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx index 701fe1d30..0c969e5ed 100644 --- a/webapp/components/signup/signup_controller.jsx +++ b/webapp/components/signup/signup_controller.jsx @@ -13,6 +13,7 @@ import * as AsyncClient from 'utils/async_client.jsx'; import Client from 'client/web_client.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {addUserToTeamFromInvite, getInviteInfo} from 'actions/team_actions.jsx'; +import {loadMe} from 'actions/user_actions.jsx'; import logoImage from 'images/logo.png'; import ErrorBar from 'components/error_bar.jsx'; @@ -74,7 +75,7 @@ export default class SignupController extends React.Component { hash, inviteId, (team) => { - GlobalActions.emitInitialLoad( + loadMe( () => { browserHistory.push('/' + team.name + '/channels/town-square'); } diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx index ddf9d46e7..4d55e9db6 100644 --- a/webapp/components/suggestion/at_mention_provider.jsx +++ b/webapp/components/suggestion/at_mention_provider.jsx @@ -127,14 +127,14 @@ export default class AtMentionProvider extends Provider { return; } - const members = data.in_channel; + const members = Object.assign([], data.users); for (const id of Object.keys(members)) { - members[id].type = Constants.MENTION_MEMBERS; + members[id] = {...members[id], type: Constants.MENTION_MEMBERS}; } - const nonmembers = data.out_of_channel; + const nonmembers = data.out_of_channel || []; for (const id of Object.keys(nonmembers)) { - nonmembers[id].type = Constants.MENTION_NONMEMBERS; + nonmembers[id] = {...nonmembers[id], type: Constants.MENTION_NONMEMBERS}; } let specialMentions = []; diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx index 6fcc7f7e9..d55f35c87 100644 --- a/webapp/components/suggestion/search_user_provider.jsx +++ b/webapp/components/suggestion/search_user_provider.jsx @@ -72,7 +72,7 @@ export default class SearchUserProvider extends Provider { return; } - const users = data.in_team; + const users = Object.assign([], data.users); const mentions = users.map((user) => user.username); AppDispatcher.handleServerAction({ diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx index 3d295951c..67cda61ea 100644 --- a/webapp/components/suggestion/switch_channel_provider.jsx +++ b/webapp/components/suggestion/switch_channel_provider.jsx @@ -67,7 +67,9 @@ export default class SwitchChannelProvider extends Provider { autocompleteUsers( channelPrefix, - (users) => { + (data) => { + const users = Object.assign([], data.users); + if (this.shouldCancelDispatch(channelPrefix)) { return; } diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx deleted file mode 100644 index 7c2b763c3..000000000 --- a/webapp/components/team_members_dropdown.jsx +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ConfirmModal from './confirm_modal.jsx'; - -import TeamStore from 'stores/team_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; - -import {removeUserFromTeam, updateTeamMemberRoles} from 'actions/team_actions.jsx'; -import {loadMyTeamMembers, updateActive} from 'actions/user_actions.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router/es6'; - -export default class TeamMembersDropdown extends React.Component { - constructor(props) { - super(props); - - this.handleMakeMember = this.handleMakeMember.bind(this); - this.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this); - this.handleMakeActive = this.handleMakeActive.bind(this); - this.handleMakeNotActive = this.handleMakeNotActive.bind(this); - this.handleMakeAdmin = this.handleMakeAdmin.bind(this); - this.handleDemote = this.handleDemote.bind(this); - this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); - this.handleDemoteCancel = this.handleDemoteCancel.bind(this); - - this.state = { - serverError: null, - showDemoteModal: false, - user: null, - role: null - }; - } - - handleMakeMember() { - const me = UserStore.getCurrentUser(); - if (this.props.user.id === me.id && me.roles.includes('system_admin')) { - this.handleDemote(this.props.user, 'team_user'); - } else { - updateTeamMemberRoles( - this.props.teamMember.team_id, - this.props.user.id, - 'team_user', - () => { - AsyncClient.getUser(this.props.user.id); - - if (this.props.user.id === me.id) { - loadMyTeamMembers(); - } - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - } - - handleRemoveFromTeam() { - removeUserFromTeam( - this.props.teamMember.team_id, - this.props.user.id, - () => { - UserStore.removeProfileFromTeam(this.props.teamMember.team_id, this.props.user.id); - UserStore.emitInTeamChange(); - AsyncClient.getTeamStats(this.props.teamMember.team_id); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - handleMakeActive() { - updateActive(this.props.user.id, true, - () => { - AsyncClient.getChannelStats(ChannelStore.getCurrentId()); - AsyncClient.getTeamStats(this.props.teamMember.team_id); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - handleMakeNotActive() { - updateActive(this.props.user.id, false, - () => { - AsyncClient.getChannelStats(ChannelStore.getCurrentId()); - AsyncClient.getTeamStats(this.props.teamMember.team_id); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - handleMakeAdmin() { - const me = UserStore.getCurrentUser(); - if (this.props.user.id === me.id && me.roles.includes('system_admin')) { - this.handleDemote(this.props.user, 'team_user team_admin'); - } else { - updateTeamMemberRoles( - this.props.teamMember.team_id, - this.props.user.id, - 'team_user team_admin', - () => { - AsyncClient.getUser(this.props.user.id); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - } - - handleDemote(user, role, newRole) { - this.setState({ - serverError: this.state.serverError, - showDemoteModal: true, - user, - role, - newRole - }); - } - - handleDemoteCancel() { - this.setState({ - serverError: null, - showDemoteModal: false, - user: null, - role: null, - newRole: null - }); - } - - handleDemoteSubmit() { - updateTeamMemberRoles( - this.props.teamMember.team_id, - this.props.user.id, - this.state.newRole, - () => { - AsyncClient.getUser(this.props.user.id); - - const teamUrl = TeamStore.getCurrentTeamUrl(); - if (teamUrl) { - browserHistory.push(teamUrl); - } else { - browserHistory.push('/'); - } - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - render() { - let serverError = null; - if (this.state.serverError) { - serverError = ( -
- -
- ); - } - - const teamMember = this.props.teamMember; - const user = this.props.user; - let currentRoles = ( - - ); - - if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) { - currentRoles = ( - - ); - } - - if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) { - currentRoles = ( - - ); - } - - const me = UserStore.getCurrentUser(); - let showMakeMember = Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); - let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); - let showMakeActive = false; - let showMakeNotActive = Utils.isSystemAdmin(user.roles); - - if (user.delete_at > 0) { - currentRoles = ( - - ); - showMakeMember = false; - showMakeAdmin = false; - showMakeActive = true; - showMakeNotActive = false; - } - - let makeAdmin = null; - if (showMakeAdmin) { - makeAdmin = ( -
  • - - - -
  • - ); - } - - let makeMember = null; - if (showMakeMember) { - makeMember = ( -
  • - - - -
  • - ); - } - - let removeFromTeam = null; - if (this.props.user.id !== me.id) { - removeFromTeam = ( -
  • - - - -
  • - ); - } - - const makeActive = null; - if (showMakeActive) { - // makeActive = ( - //
  • - // - // - // - //
  • - // ); - } - - const makeNotActive = null; - if (showMakeNotActive) { - // makeNotActive = ( - //
  • - // - // - // - //
  • - // ); - } - - let makeDemoteModal = null; - if (this.props.user.id === me.id) { - const title = ( - - ); - - const message = ( -
    - -
    -
    - - {serverError} -
    - ); - - const confirmButton = ( - - ); - - makeDemoteModal = ( - - ); - } - - if (!removeFromTeam && !makeAdmin && !makeMember && !makeActive && !makeNotActive) { - return
    {currentRoles}
    ; - } - - return ( -
    - - {currentRoles} - - -
      - {removeFromTeam} - {makeAdmin} - {makeMember} - {makeActive} - {makeNotActive} -
    - {makeDemoteModal} - {serverError} -
    - ); - } -} - -TeamMembersDropdown.propTypes = { - user: React.PropTypes.object.isRequired, - teamMember: React.PropTypes.object.isRequired -}; diff --git a/webapp/components/team_members_dropdown/index.js b/webapp/components/team_members_dropdown/index.js new file mode 100644 index 000000000..54e002a6e --- /dev/null +++ b/webapp/components/team_members_dropdown/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getUser} from 'mattermost-redux/actions/users'; + +import TeamMembersDropdown from './team_members_dropdown.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getUser + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembersDropdown); diff --git a/webapp/components/team_members_dropdown/team_members_dropdown.jsx b/webapp/components/team_members_dropdown/team_members_dropdown.jsx new file mode 100644 index 000000000..704a60dae --- /dev/null +++ b/webapp/components/team_members_dropdown/team_members_dropdown.jsx @@ -0,0 +1,395 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ConfirmModal from 'components/confirm_modal.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import {removeUserFromTeam, updateTeamMemberRoles} from 'actions/team_actions.jsx'; +import {loadMyTeamMembers, updateActive} from 'actions/user_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +export default class TeamMembersDropdown extends React.Component { + static propTypes = { + user: React.PropTypes.object.isRequired, + teamMember: React.PropTypes.object.isRequired, + actions: React.PropTypes.shape({ + getUser: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.handleMakeMember = this.handleMakeMember.bind(this); + this.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this); + this.handleMakeActive = this.handleMakeActive.bind(this); + this.handleMakeNotActive = this.handleMakeNotActive.bind(this); + this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + this.handleDemote = this.handleDemote.bind(this); + this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); + this.handleDemoteCancel = this.handleDemoteCancel.bind(this); + + this.state = { + serverError: null, + showDemoteModal: false, + user: null, + role: null + }; + } + + handleMakeMember() { + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id && me.roles.includes('system_admin')) { + this.handleDemote(this.props.user, 'team_user'); + } else { + updateTeamMemberRoles( + this.props.teamMember.team_id, + this.props.user.id, + 'team_user', + () => { + this.props.actions.getUser(this.props.user.id); + + if (this.props.user.id === me.id) { + loadMyTeamMembers(); + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + + handleRemoveFromTeam() { + removeUserFromTeam( + this.props.teamMember.team_id, + this.props.user.id, + () => { + UserStore.removeProfileFromTeam(this.props.teamMember.team_id, this.props.user.id); + UserStore.emitInTeamChange(); + AsyncClient.getTeamStats(this.props.teamMember.team_id); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeActive() { + updateActive(this.props.user.id, true, + () => { + AsyncClient.getChannelStats(ChannelStore.getCurrentId()); + AsyncClient.getTeamStats(this.props.teamMember.team_id); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeNotActive() { + updateActive(this.props.user.id, false, + () => { + AsyncClient.getChannelStats(ChannelStore.getCurrentId()); + AsyncClient.getTeamStats(this.props.teamMember.team_id); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeAdmin() { + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id && me.roles.includes('system_admin')) { + this.handleDemote(this.props.user, 'team_user team_admin'); + } else { + updateTeamMemberRoles( + this.props.teamMember.team_id, + this.props.user.id, + 'team_user team_admin', + () => { + this.props.actions.getUser(this.props.user.id); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + + handleDemote(user, role, newRole) { + this.setState({ + serverError: this.state.serverError, + showDemoteModal: true, + user, + role, + newRole + }); + } + + handleDemoteCancel() { + this.setState({ + serverError: null, + showDemoteModal: false, + user: null, + role: null, + newRole: null + }); + } + + handleDemoteSubmit() { + updateTeamMemberRoles( + this.props.teamMember.team_id, + this.props.user.id, + this.state.newRole, + () => { + this.props.actions.getUser(this.props.user.id); + + const teamUrl = TeamStore.getCurrentTeamUrl(); + if (teamUrl) { + browserHistory.push(teamUrl); + } else { + browserHistory.push('/'); + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + const teamMember = this.props.teamMember; + const user = this.props.user; + let currentRoles = ( + + ); + + if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) { + currentRoles = ( + + ); + } + + if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) { + currentRoles = ( + + ); + } + + const me = UserStore.getCurrentUser(); + let showMakeMember = Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); + let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); + let showMakeActive = false; + let showMakeNotActive = Utils.isSystemAdmin(user.roles); + + if (user.delete_at > 0) { + currentRoles = ( + + ); + showMakeMember = false; + showMakeAdmin = false; + showMakeActive = true; + showMakeNotActive = false; + } + + let makeAdmin = null; + if (showMakeAdmin) { + makeAdmin = ( +
  • + + + +
  • + ); + } + + let makeMember = null; + if (showMakeMember) { + makeMember = ( +
  • + + + +
  • + ); + } + + let removeFromTeam = null; + if (this.props.user.id !== me.id) { + removeFromTeam = ( +
  • + + + +
  • + ); + } + + const makeActive = null; + if (showMakeActive) { + // makeActive = ( + //
  • + // + // + // + //
  • + // ); + } + + const makeNotActive = null; + if (showMakeNotActive) { + // makeNotActive = ( + //
  • + // + // + // + //
  • + // ); + } + + let makeDemoteModal = null; + if (this.props.user.id === me.id) { + const title = ( + + ); + + const message = ( +
    + +
    +
    + + {serverError} +
    + ); + + const confirmButton = ( + + ); + + makeDemoteModal = ( + + ); + } + + if (!removeFromTeam && !makeAdmin && !makeMember && !makeActive && !makeNotActive) { + return
    {currentRoles}
    ; + } + + return ( +
    + + {currentRoles} + + +
      + {removeFromTeam} + {makeAdmin} + {makeMember} + {makeActive} + {makeNotActive} +
    + {makeDemoteModal} + {serverError} +
    + ); + } +} diff --git a/webapp/components/user_settings/user_settings.jsx b/webapp/components/user_settings/user_settings.jsx index d9d5423fe..b01274b32 100644 --- a/webapp/components/user_settings/user_settings.jsx +++ b/webapp/components/user_settings/user_settings.jsx @@ -4,8 +4,8 @@ import UserStore from 'stores/user_store.jsx'; import * as utils from 'utils/utils.jsx'; import NotificationsTab from './user_settings_notifications.jsx'; -import SecurityTab from './user_settings_security.jsx'; -import GeneralTab from './user_settings_general.jsx'; +import SecurityTab from './user_settings_security'; +import GeneralTab from './user_settings_general'; import DisplayTab from './user_settings_display.jsx'; import AdvancedTab from './user_settings_advanced.jsx'; diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx deleted file mode 100644 index ce4349519..000000000 --- a/webapp/components/user_settings/user_settings_general.jsx +++ /dev/null @@ -1,1207 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import SettingItemMin from '../setting_item_min.jsx'; -import SettingItemMax from '../setting_item_max.jsx'; -import SettingPicture from '../setting_picture.jsx'; - -import UserStore from 'stores/user_store.jsx'; -import ErrorStore from 'stores/error_store.jsx'; - -import Client from 'client/web_client.jsx'; -import Constants from 'utils/constants.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl'; -import {updateUser, uploadProfileImage} from 'actions/user_actions.jsx'; -import {trackEvent} from 'actions/diagnostics_actions.jsx'; - -const holders = defineMessages({ - usernameReserved: { - id: 'user.settings.general.usernameReserved', - defaultMessage: 'This username is reserved, please choose a new one.' - }, - usernameRestrictions: { - id: 'user.settings.general.usernameRestrictions', - defaultMessage: "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-', and '_'." - }, - validEmail: { - id: 'user.settings.general.validEmail', - defaultMessage: 'Please enter a valid email address.' - }, - emailMatch: { - id: 'user.settings.general.emailMatch', - defaultMessage: 'The new emails you entered do not match.' - }, - checkEmail: { - id: 'user.settings.general.checkEmail', - defaultMessage: 'Check your email at {email} to verify the address.' - }, - validImage: { - id: 'user.settings.general.validImage', - defaultMessage: 'Only JPG or PNG images may be used for profile pictures' - }, - imageTooLarge: { - id: 'user.settings.general.imageTooLarge', - defaultMessage: 'Unable to upload profile image. File is too large.' - }, - uploadImage: { - id: 'user.settings.general.uploadImage', - defaultMessage: "Click 'Edit' to upload an image." - }, - fullName: { - id: 'user.settings.general.fullName', - defaultMessage: 'Full Name' - }, - nickname: { - id: 'user.settings.general.nickname', - defaultMessage: 'Nickname' - }, - username: { - id: 'user.settings.general.username', - defaultMessage: 'Username' - }, - profilePicture: { - id: 'user.settings.general.profilePicture', - defaultMessage: 'Profile Picture' - }, - close: { - id: 'user.settings.general.close', - defaultMessage: 'Close' - }, - position: { - id: 'user.settings.general.position', - defaultMessage: 'Position' - } -}); - -import React from 'react'; - -class UserSettingsGeneralTab extends React.Component { - constructor(props) { - super(props); - this.submitActive = false; - - this.submitUsername = this.submitUsername.bind(this); - this.submitNickname = this.submitNickname.bind(this); - this.submitName = this.submitName.bind(this); - this.submitEmail = this.submitEmail.bind(this); - this.submitUser = this.submitUser.bind(this); - this.submitPicture = this.submitPicture.bind(this); - this.submitPosition = this.submitPosition.bind(this); - - this.updateUsername = this.updateUsername.bind(this); - this.updateFirstName = this.updateFirstName.bind(this); - this.updateLastName = this.updateLastName.bind(this); - this.updateNickname = this.updateNickname.bind(this); - this.updateEmail = this.updateEmail.bind(this); - this.updateConfirmEmail = this.updateConfirmEmail.bind(this); - this.updatePicture = this.updatePicture.bind(this); - this.updateSection = this.updateSection.bind(this); - this.updatePosition = this.updatePosition.bind(this); - this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this); - - this.state = this.setupInitialState(props); - } - - submitUsername(e) { - e.preventDefault(); - - const user = Object.assign({}, this.props.user); - const username = this.state.username.trim().toLowerCase(); - - const {formatMessage} = this.props.intl; - const usernameError = Utils.isValidUsername(username); - if (usernameError === 'Cannot use a reserved word as a username.') { - this.setState({clientError: formatMessage(holders.usernameReserved)}); - return; - } else if (usernameError) { - this.setState({clientError: formatMessage(holders.usernameRestrictions, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH})}); - return; - } - - if (user.username === username) { - this.updateSection(''); - return; - } - - user.username = username; - - trackEvent('settings', 'user_settings_update', {field: 'username'}); - - this.submitUser(user, Constants.UserUpdateEvents.USERNAME, false); - } - - submitNickname(e) { - e.preventDefault(); - - const user = Object.assign({}, this.props.user); - const nickname = this.state.nickname.trim(); - - if (user.nickname === nickname) { - this.updateSection(''); - return; - } - - user.nickname = nickname; - - trackEvent('settings', 'user_settings_update', {field: 'username'}); - - this.submitUser(user, Constants.UserUpdateEvents.NICKNAME, false); - } - - submitName(e) { - e.preventDefault(); - - const user = Object.assign({}, this.props.user); - const firstName = this.state.firstName.trim(); - const lastName = this.state.lastName.trim(); - - if (user.first_name === firstName && user.last_name === lastName) { - this.updateSection(''); - return; - } - - user.first_name = firstName; - user.last_name = lastName; - - trackEvent('settings', 'user_settings_update', {field: 'fullname'}); - - this.submitUser(user, Constants.UserUpdateEvents.FULLNAME, false); - } - - submitEmail(e) { - e.preventDefault(); - - const user = Object.assign({}, this.props.user); - const email = this.state.email.trim().toLowerCase(); - const confirmEmail = this.state.confirmEmail.trim().toLowerCase(); - - const {formatMessage} = this.props.intl; - - if (email === user.email && (confirmEmail === '' || confirmEmail === user.email)) { - this.updateSection(''); - return; - } - - if (email === '' || !Utils.isEmail(email)) { - this.setState({emailError: formatMessage(holders.validEmail), clientError: '', serverError: ''}); - return; - } - - if (email !== confirmEmail) { - this.setState({emailError: formatMessage(holders.emailMatch), clientError: '', serverError: ''}); - return; - } - - user.email = email; - trackEvent('settings', 'user_settings_update', {field: 'email'}); - this.submitUser(user, Constants.UserUpdateEvents.EMAIL, true); - } - - submitUser(user, type, emailUpdated) { - updateUser(user, type, - () => { - this.updateSection(''); - AsyncClient.getMe(); - const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated; - - if (verificationEnabled) { - ErrorStore.storeLastError({message: this.props.intl.formatMessage(holders.checkEmail, {email: user.email})}); - ErrorStore.emitChange(); - this.setState({emailChangeInProgress: true}); - } - }, - (err) => { - let serverError; - if (err.message) { - serverError = err.message; - } else { - serverError = err; - } - this.setState({serverError, emailError: '', clientError: ''}); - } - ); - } - - submitPicture(e) { - e.preventDefault(); - - if (!this.state.picture) { - return; - } - - if (!this.submitActive) { - return; - } - - trackEvent('settings', 'user_settings_update', {field: 'picture'}); - - const {formatMessage} = this.props.intl; - const picture = this.state.picture; - - if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { - this.setState({clientError: formatMessage(holders.validImage)}); - return; - } else if (picture.size > this.state.maxFileSize) { - this.setState({clientError: formatMessage(holders.imageTooLarge)}); - return; - } - - this.setState({loadingPicture: true}); - - uploadProfileImage( - picture, - () => { - this.updateSection(''); - this.submitActive = false; - }, - (err) => { - var state = this.setupInitialState(this.props); - state.serverError = err.message; - this.setState(state); - } - ); - } - - submitPosition(e) { - e.preventDefault(); - - const user = Object.assign({}, this.props.user); - const position = this.state.position.trim(); - - if (user.position === position) { - this.updateSection(''); - return; - } - - user.position = position; - - trackEvent('settings', 'user_settings_update', {field: 'position'}); - - this.submitUser(user, Constants.UserUpdateEvents.Position, false); - } - - updateUsername(e) { - this.setState({username: e.target.value}); - } - - updateFirstName(e) { - this.setState({firstName: e.target.value}); - } - - updateLastName(e) { - this.setState({lastName: e.target.value}); - } - - updateNickname(e) { - this.setState({nickname: e.target.value}); - } - - updatePosition(e) { - this.setState({position: e.target.value}); - } - - updateEmail(e) { - this.setState({email: e.target.value}); - } - - updateConfirmEmail(e) { - this.setState({confirmEmail: e.target.value}); - } - - updatedCroppedPicture(file) { - if (file) { - this.setState({picture: file}); - - this.submitActive = true; - this.setState({clientError: null}); - } else { - this.setState({picture: null}); - } - } - - updatePicture(e) { - if (e.target.files && e.target.files[0]) { - this.setState({picture: e.target.files[0]}); - - this.submitActive = true; - this.setState({clientError: null}); - } else { - this.setState({picture: null}); - } - } - - updateSection(section) { - if ($('.section-max').length) { - $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); - } - const emailChangeInProgress = this.state.emailChangeInProgress; - this.setState(Object.assign({}, this.setupInitialState(this.props), {emailChangeInProgress, clientError: '', serverError: '', emailError: ''})); - this.submitActive = false; - this.props.updateSection(section); - } - - setupInitialState(props) { - const user = props.user; - - return { - username: user.username, - firstName: user.first_name, - lastName: user.last_name, - nickname: user.nickname, - position: user.position, - email: user.email, - confirmEmail: '', - picture: null, - loadingPicture: false, - emailChangeInProgress: false, - maxFileSize: global.window.mm_config.MaxFileSize - }; - } - - createEmailSection() { - let emailSection; - - if (this.props.activeSection === 'email') { - const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; - const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; - const inputs = []; - - let helpText = ( - - ); - - if (!emailEnabled) { - helpText = ( -
    - -
    - ); - } else if (!emailVerificationEnabled) { - helpText = ( - - ); - } else if (this.state.emailChangeInProgress) { - const newEmail = UserStore.getCurrentUser().email; - if (newEmail) { - helpText = ( - - ); - } - } - - let submit = null; - - if (this.props.user.auth_service === '') { - inputs.push( -
    -
    - -
    - -
    -
    -
    - ); - - inputs.push( -
    -
    - -
    - -
    -
    - {helpText} -
    - ); - - submit = this.submitEmail; - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - inputs.push( -
    -
    - -
    - {helpText} -
    - ); - } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { - inputs.push( -
    -
    - -
    - {helpText} -
    - ); - } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { - inputs.push( -
    -
    - -
    - {helpText} -
    - ); - } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { - inputs.push( -
    -
    - -
    - {helpText} -
    - ); - } - - emailSection = ( - - } - inputs={inputs} - submit={submit} - server_error={this.state.serverError} - client_error={this.state.emailError} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - let describe = ''; - if (this.props.user.auth_service === '') { - if (this.state.emailChangeInProgress) { - const newEmail = UserStore.getCurrentUser().email; - if (newEmail) { - describe = ( - - ); - } else { - describe = ( - - ); - } - } else { - describe = UserStore.getCurrentUser().email; - } - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { - describe = ( - - ); - } - - emailSection = ( - - } - describe={describe} - updateSection={() => { - this.updateSection('email'); - }} - /> - ); - } - - return emailSection; - } - - render() { - const user = this.props.user; - const {formatMessage} = this.props.intl; - - let clientError = null; - if (this.state.clientError) { - clientError = this.state.clientError; - } - let serverError = null; - if (this.state.serverError) { - serverError = this.state.serverError; - } - - let nameSection; - const inputs = []; - - if (this.props.activeSection === 'name') { - let extraInfo; - let submit = null; - if (this.props.user.auth_service === '' || - ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && - (global.window.mm_config.FirstNameAttributeSet === 'false' || global.window.mm_config.LastNameAttributeSet === 'false'))) { - inputs.push( -
    - -
    - -
    -
    - ); - - inputs.push( -
    - -
    - -
    -
    - ); - - function notifClick(e) { - e.preventDefault(); - this.updateSection(''); - this.props.updateTab('notifications'); - } - - const notifLink = ( - - - - ); - - extraInfo = ( - - - - ); - - submit = this.submitName; - } else { - extraInfo = ( - - - - ); - } - - nameSection = ( - { - this.updateSection(''); - e.preventDefault(); - }} - extraInfo={extraInfo} - /> - ); - } else { - let describe = ''; - - if (user.first_name && user.last_name) { - describe = user.first_name + ' ' + user.last_name; - } else if (user.first_name) { - describe = user.first_name; - } else if (user.last_name) { - describe = user.last_name; - } else { - describe = ( - - ); - } - - nameSection = ( - { - this.updateSection('name'); - }} - /> - ); - } - - let nicknameSection; - if (this.props.activeSection === 'nickname') { - let extraInfo; - let submit = null; - if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.NicknameAttributeSet === 'true') { - extraInfo = ( - - - - ); - } else { - let nicknameLabel = ( - - ); - if (Utils.isMobile()) { - nicknameLabel = ''; - } - - inputs.push( -
    - -
    - -
    -
    - ); - - extraInfo = ( - - - - ); - - submit = this.submitNickname; - } - - nicknameSection = ( - { - this.updateSection(''); - e.preventDefault(); - }} - extraInfo={extraInfo} - /> - ); - } else { - let describe = ''; - if (user.nickname) { - describe = user.nickname; - } else { - describe = ( - - ); - } - - nicknameSection = ( - { - this.updateSection('nickname'); - }} - /> - ); - } - - let usernameSection; - if (this.props.activeSection === 'username') { - let extraInfo; - let submit = null; - if (this.props.user.auth_service === '') { - let usernameLabel = ( - - ); - if (Utils.isMobile()) { - usernameLabel = ''; - } - - inputs.push( -
    - -
    - -
    -
    - ); - - extraInfo = ( - - - - ); - - submit = this.submitUsername; - } else { - extraInfo = ( - - - - ); - } - - usernameSection = ( - { - this.updateSection(''); - e.preventDefault(); - }} - extraInfo={extraInfo} - /> - ); - } else { - usernameSection = ( - { - this.updateSection('username'); - }} - /> - ); - } - - let positionSection; - if (this.props.activeSection === 'position') { - let extraInfo; - let submit = null; - if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.PositionAttributeSet === 'true') { - extraInfo = ( - - - - ); - } else { - let positionLabel = ( - - ); - if (Utils.isMobile()) { - positionLabel = ''; - } - - inputs.push( -
    - -
    - -
    -
    - ); - - extraInfo = ( - - - - ); - - submit = this.submitPosition; - } - - positionSection = ( - { - this.updateSection(''); - e.preventDefault(); - }} - extraInfo={extraInfo} - /> - ); - } else { - let describe = ''; - if (user.position) { - describe = user.position; - } else { - describe = ( - - ); - } - - positionSection = ( - { - this.updateSection('position'); - }} - /> - ); - } - - const emailSection = this.createEmailSection(); - - let pictureSection; - if (this.props.activeSection === 'picture') { - pictureSection = ( - - ); - } else { - let minMessage = formatMessage(holders.uploadImage); - if (user.last_picture_update) { - minMessage = ( - - ) - }} - /> - ); - } - pictureSection = ( - { - this.updateSection('picture'); - }} - /> - ); - } - - return ( -
    -
    - -

    -
    - -
    - -

    -
    -
    -

    - -

    -
    - {nameSection} -
    - {usernameSection} -
    - {nicknameSection} -
    - {positionSection} -
    - {emailSection} -
    - {pictureSection} -
    -
    -
    - ); - } -} - -UserSettingsGeneralTab.propTypes = { - intl: intlShape.isRequired, - user: React.PropTypes.object.isRequired, - updateSection: React.PropTypes.func.isRequired, - updateTab: React.PropTypes.func.isRequired, - activeSection: React.PropTypes.string.isRequired, - closeModal: React.PropTypes.func.isRequired, - collapseModal: React.PropTypes.func.isRequired -}; - -export default injectIntl(UserSettingsGeneralTab); diff --git a/webapp/components/user_settings/user_settings_general/index.js b/webapp/components/user_settings/user_settings_general/index.js new file mode 100644 index 000000000..90fd58bf2 --- /dev/null +++ b/webapp/components/user_settings/user_settings_general/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getMe} from 'mattermost-redux/actions/users'; + +import UserSettingsGeneralTab from './user_settings_general.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getMe + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsGeneralTab); diff --git a/webapp/components/user_settings/user_settings_general/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general/user_settings_general.jsx new file mode 100644 index 000000000..79132d929 --- /dev/null +++ b/webapp/components/user_settings/user_settings_general/user_settings_general.jsx @@ -0,0 +1,1209 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import SettingItemMin from 'components/setting_item_min.jsx'; +import SettingItemMax from 'components/setting_item_max.jsx'; +import SettingPicture from 'components/setting_picture.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import ErrorStore from 'stores/error_store.jsx'; + +import Client from 'client/web_client.jsx'; +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl'; +import {updateUser, uploadProfileImage} from 'actions/user_actions.jsx'; +import {trackEvent} from 'actions/diagnostics_actions.jsx'; + +const holders = defineMessages({ + usernameReserved: { + id: 'user.settings.general.usernameReserved', + defaultMessage: 'This username is reserved, please choose a new one.' + }, + usernameRestrictions: { + id: 'user.settings.general.usernameRestrictions', + defaultMessage: "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-', and '_'." + }, + validEmail: { + id: 'user.settings.general.validEmail', + defaultMessage: 'Please enter a valid email address.' + }, + emailMatch: { + id: 'user.settings.general.emailMatch', + defaultMessage: 'The new emails you entered do not match.' + }, + checkEmail: { + id: 'user.settings.general.checkEmail', + defaultMessage: 'Check your email at {email} to verify the address.' + }, + validImage: { + id: 'user.settings.general.validImage', + defaultMessage: 'Only JPG or PNG images may be used for profile pictures' + }, + imageTooLarge: { + id: 'user.settings.general.imageTooLarge', + defaultMessage: 'Unable to upload profile image. File is too large.' + }, + uploadImage: { + id: 'user.settings.general.uploadImage', + defaultMessage: "Click 'Edit' to upload an image." + }, + fullName: { + id: 'user.settings.general.fullName', + defaultMessage: 'Full Name' + }, + nickname: { + id: 'user.settings.general.nickname', + defaultMessage: 'Nickname' + }, + username: { + id: 'user.settings.general.username', + defaultMessage: 'Username' + }, + profilePicture: { + id: 'user.settings.general.profilePicture', + defaultMessage: 'Profile Picture' + }, + close: { + id: 'user.settings.general.close', + defaultMessage: 'Close' + }, + position: { + id: 'user.settings.general.position', + defaultMessage: 'Position' + } +}); + +import React from 'react'; + +class UserSettingsGeneralTab extends React.Component { + static propTypes = { + intl: intlShape.isRequired, + user: React.PropTypes.object.isRequired, + updateSection: React.PropTypes.func.isRequired, + updateTab: React.PropTypes.func.isRequired, + activeSection: React.PropTypes.string.isRequired, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired, + actions: React.PropTypes.shape({ + getMe: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + this.submitActive = false; + + this.submitUsername = this.submitUsername.bind(this); + this.submitNickname = this.submitNickname.bind(this); + this.submitName = this.submitName.bind(this); + this.submitEmail = this.submitEmail.bind(this); + this.submitUser = this.submitUser.bind(this); + this.submitPicture = this.submitPicture.bind(this); + this.submitPosition = this.submitPosition.bind(this); + + this.updateUsername = this.updateUsername.bind(this); + this.updateFirstName = this.updateFirstName.bind(this); + this.updateLastName = this.updateLastName.bind(this); + this.updateNickname = this.updateNickname.bind(this); + this.updateEmail = this.updateEmail.bind(this); + this.updateConfirmEmail = this.updateConfirmEmail.bind(this); + this.updatePicture = this.updatePicture.bind(this); + this.updateSection = this.updateSection.bind(this); + this.updatePosition = this.updatePosition.bind(this); + this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this); + + this.state = this.setupInitialState(props); + } + + submitUsername(e) { + e.preventDefault(); + + const user = Object.assign({}, this.props.user); + const username = this.state.username.trim().toLowerCase(); + + const {formatMessage} = this.props.intl; + const usernameError = Utils.isValidUsername(username); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({clientError: formatMessage(holders.usernameReserved)}); + return; + } else if (usernameError) { + this.setState({clientError: formatMessage(holders.usernameRestrictions, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH})}); + return; + } + + if (user.username === username) { + this.updateSection(''); + return; + } + + user.username = username; + + trackEvent('settings', 'user_settings_update', {field: 'username'}); + + this.submitUser(user, Constants.UserUpdateEvents.USERNAME, false); + } + + submitNickname(e) { + e.preventDefault(); + + const user = Object.assign({}, this.props.user); + const nickname = this.state.nickname.trim(); + + if (user.nickname === nickname) { + this.updateSection(''); + return; + } + + user.nickname = nickname; + + trackEvent('settings', 'user_settings_update', {field: 'username'}); + + this.submitUser(user, Constants.UserUpdateEvents.NICKNAME, false); + } + + submitName(e) { + e.preventDefault(); + + const user = Object.assign({}, this.props.user); + const firstName = this.state.firstName.trim(); + const lastName = this.state.lastName.trim(); + + if (user.first_name === firstName && user.last_name === lastName) { + this.updateSection(''); + return; + } + + user.first_name = firstName; + user.last_name = lastName; + + trackEvent('settings', 'user_settings_update', {field: 'fullname'}); + + this.submitUser(user, Constants.UserUpdateEvents.FULLNAME, false); + } + + submitEmail(e) { + e.preventDefault(); + + const user = Object.assign({}, this.props.user); + const email = this.state.email.trim().toLowerCase(); + const confirmEmail = this.state.confirmEmail.trim().toLowerCase(); + + const {formatMessage} = this.props.intl; + + if (email === user.email && (confirmEmail === '' || confirmEmail === user.email)) { + this.updateSection(''); + return; + } + + if (email === '' || !Utils.isEmail(email)) { + this.setState({emailError: formatMessage(holders.validEmail), clientError: '', serverError: ''}); + return; + } + + if (email !== confirmEmail) { + this.setState({emailError: formatMessage(holders.emailMatch), clientError: '', serverError: ''}); + return; + } + + user.email = email; + trackEvent('settings', 'user_settings_update', {field: 'email'}); + this.submitUser(user, Constants.UserUpdateEvents.EMAIL, true); + } + + submitUser(user, type, emailUpdated) { + updateUser(user, type, + () => { + this.updateSection(''); + this.props.actions.getMe(); + const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated; + + if (verificationEnabled) { + ErrorStore.storeLastError({message: this.props.intl.formatMessage(holders.checkEmail, {email: user.email})}); + ErrorStore.emitChange(); + this.setState({emailChangeInProgress: true}); + } + }, + (err) => { + let serverError; + if (err.message) { + serverError = err.message; + } else { + serverError = err; + } + this.setState({serverError, emailError: '', clientError: ''}); + } + ); + } + + submitPicture(e) { + e.preventDefault(); + + if (!this.state.picture) { + return; + } + + if (!this.submitActive) { + return; + } + + trackEvent('settings', 'user_settings_update', {field: 'picture'}); + + const {formatMessage} = this.props.intl; + const picture = this.state.picture; + + if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { + this.setState({clientError: formatMessage(holders.validImage)}); + return; + } else if (picture.size > this.state.maxFileSize) { + this.setState({clientError: formatMessage(holders.imageTooLarge)}); + return; + } + + this.setState({loadingPicture: true}); + + uploadProfileImage( + picture, + () => { + this.updateSection(''); + this.submitActive = false; + }, + (err) => { + var state = this.setupInitialState(this.props); + state.serverError = err.message; + this.setState(state); + } + ); + } + + submitPosition(e) { + e.preventDefault(); + + const user = Object.assign({}, this.props.user); + const position = this.state.position.trim(); + + if (user.position === position) { + this.updateSection(''); + return; + } + + user.position = position; + + trackEvent('settings', 'user_settings_update', {field: 'position'}); + + this.submitUser(user, Constants.UserUpdateEvents.Position, false); + } + + updateUsername(e) { + this.setState({username: e.target.value}); + } + + updateFirstName(e) { + this.setState({firstName: e.target.value}); + } + + updateLastName(e) { + this.setState({lastName: e.target.value}); + } + + updateNickname(e) { + this.setState({nickname: e.target.value}); + } + + updatePosition(e) { + this.setState({position: e.target.value}); + } + + updateEmail(e) { + this.setState({email: e.target.value}); + } + + updateConfirmEmail(e) { + this.setState({confirmEmail: e.target.value}); + } + + updatedCroppedPicture(file) { + if (file) { + this.setState({picture: file}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + } + + updatePicture(e) { + if (e.target.files && e.target.files[0]) { + this.setState({picture: e.target.files[0]}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + } + + updateSection(section) { + if ($('.section-max').length) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + } + const emailChangeInProgress = this.state.emailChangeInProgress; + this.setState(Object.assign({}, this.setupInitialState(this.props), {emailChangeInProgress, clientError: '', serverError: '', emailError: ''})); + this.submitActive = false; + this.props.updateSection(section); + } + + setupInitialState(props) { + const user = props.user; + + return { + username: user.username, + firstName: user.first_name, + lastName: user.last_name, + nickname: user.nickname, + position: user.position, + email: user.email, + confirmEmail: '', + picture: null, + loadingPicture: false, + emailChangeInProgress: false, + maxFileSize: global.window.mm_config.MaxFileSize + }; + } + + createEmailSection() { + let emailSection; + + if (this.props.activeSection === 'email') { + const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; + const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; + const inputs = []; + + let helpText = ( + + ); + + if (!emailEnabled) { + helpText = ( +
    + +
    + ); + } else if (!emailVerificationEnabled) { + helpText = ( + + ); + } else if (this.state.emailChangeInProgress) { + const newEmail = UserStore.getCurrentUser().email; + if (newEmail) { + helpText = ( + + ); + } + } + + let submit = null; + + if (this.props.user.auth_service === '') { + inputs.push( +
    +
    + +
    + +
    +
    +
    + ); + + inputs.push( +
    +
    + +
    + +
    +
    + {helpText} +
    + ); + + submit = this.submitEmail; + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + inputs.push( +
    +
    + +
    + {helpText} +
    + ); + } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { + inputs.push( +
    +
    + +
    + {helpText} +
    + ); + } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { + inputs.push( +
    +
    + +
    + {helpText} +
    + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + inputs.push( +
    +
    + +
    + {helpText} +
    + ); + } + + emailSection = ( + + } + inputs={inputs} + submit={submit} + server_error={this.state.serverError} + client_error={this.state.emailError} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + let describe = ''; + if (this.props.user.auth_service === '') { + if (this.state.emailChangeInProgress) { + const newEmail = UserStore.getCurrentUser().email; + if (newEmail) { + describe = ( + + ); + } else { + describe = ( + + ); + } + } else { + describe = UserStore.getCurrentUser().email; + } + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + + ); + } + + emailSection = ( + + } + describe={describe} + updateSection={() => { + this.updateSection('email'); + }} + /> + ); + } + + return emailSection; + } + + render() { + const user = this.props.user; + const {formatMessage} = this.props.intl; + + let clientError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + let serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + + let nameSection; + const inputs = []; + + if (this.props.activeSection === 'name') { + let extraInfo; + let submit = null; + if (this.props.user.auth_service === '' || + ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && + (global.window.mm_config.FirstNameAttributeSet === 'false' || global.window.mm_config.LastNameAttributeSet === 'false'))) { + inputs.push( +
    + +
    + +
    +
    + ); + + inputs.push( +
    + +
    + +
    +
    + ); + + function notifClick(e) { + e.preventDefault(); + this.updateSection(''); + this.props.updateTab('notifications'); + } + + const notifLink = ( + + + + ); + + extraInfo = ( + + + + ); + + submit = this.submitName; + } else { + extraInfo = ( + + + + ); + } + + nameSection = ( + { + this.updateSection(''); + e.preventDefault(); + }} + extraInfo={extraInfo} + /> + ); + } else { + let describe = ''; + + if (user.first_name && user.last_name) { + describe = user.first_name + ' ' + user.last_name; + } else if (user.first_name) { + describe = user.first_name; + } else if (user.last_name) { + describe = user.last_name; + } else { + describe = ( + + ); + } + + nameSection = ( + { + this.updateSection('name'); + }} + /> + ); + } + + let nicknameSection; + if (this.props.activeSection === 'nickname') { + let extraInfo; + let submit = null; + if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.NicknameAttributeSet === 'true') { + extraInfo = ( + + + + ); + } else { + let nicknameLabel = ( + + ); + if (Utils.isMobile()) { + nicknameLabel = ''; + } + + inputs.push( +
    + +
    + +
    +
    + ); + + extraInfo = ( + + + + ); + + submit = this.submitNickname; + } + + nicknameSection = ( + { + this.updateSection(''); + e.preventDefault(); + }} + extraInfo={extraInfo} + /> + ); + } else { + let describe = ''; + if (user.nickname) { + describe = user.nickname; + } else { + describe = ( + + ); + } + + nicknameSection = ( + { + this.updateSection('nickname'); + }} + /> + ); + } + + let usernameSection; + if (this.props.activeSection === 'username') { + let extraInfo; + let submit = null; + if (this.props.user.auth_service === '') { + let usernameLabel = ( + + ); + if (Utils.isMobile()) { + usernameLabel = ''; + } + + inputs.push( +
    + +
    + +
    +
    + ); + + extraInfo = ( + + + + ); + + submit = this.submitUsername; + } else { + extraInfo = ( + + + + ); + } + + usernameSection = ( + { + this.updateSection(''); + e.preventDefault(); + }} + extraInfo={extraInfo} + /> + ); + } else { + usernameSection = ( + { + this.updateSection('username'); + }} + /> + ); + } + + let positionSection; + if (this.props.activeSection === 'position') { + let extraInfo; + let submit = null; + if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.PositionAttributeSet === 'true') { + extraInfo = ( + + + + ); + } else { + let positionLabel = ( + + ); + if (Utils.isMobile()) { + positionLabel = ''; + } + + inputs.push( +
    + +
    + +
    +
    + ); + + extraInfo = ( + + + + ); + + submit = this.submitPosition; + } + + positionSection = ( + { + this.updateSection(''); + e.preventDefault(); + }} + extraInfo={extraInfo} + /> + ); + } else { + let describe = ''; + if (user.position) { + describe = user.position; + } else { + describe = ( + + ); + } + + positionSection = ( + { + this.updateSection('position'); + }} + /> + ); + } + + const emailSection = this.createEmailSection(); + + let pictureSection; + if (this.props.activeSection === 'picture') { + pictureSection = ( + + ); + } else { + let minMessage = formatMessage(holders.uploadImage); + if (user.last_picture_update) { + minMessage = ( + + ) + }} + /> + ); + } + pictureSection = ( + { + this.updateSection('picture'); + }} + /> + ); + } + + return ( +
    +
    + +

    +
    + +
    + +

    +
    +
    +

    + +

    +
    + {nameSection} +
    + {usernameSection} +
    + {nicknameSection} +
    + {positionSection} +
    + {emailSection} +
    + {pictureSection} +
    +
    +
    + ); + } +} + +export default injectIntl(UserSettingsGeneralTab); diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx deleted file mode 100644 index ead579c19..000000000 --- a/webapp/components/user_settings/user_settings_security.jsx +++ /dev/null @@ -1,1033 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SettingItemMin from '../setting_item_min.jsx'; -import SettingItemMax from '../setting_item_max.jsx'; -import AccessHistoryModal from '../access_history_modal.jsx'; -import ActivityLogModal from '../activity_log_modal.jsx'; -import ToggleModalButton from '../toggle_modal_button.jsx'; - -import PreferenceStore from 'stores/preference_store.jsx'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; - -import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx'; - -import $ from 'jquery'; -import React from 'react'; -import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; -import {browserHistory, Link} from 'react-router/es6'; - -import icon50 from 'images/icon50x50.png'; - -export default class SecurityTab extends React.Component { - constructor(props) { - super(props); - - this.submitPassword = this.submitPassword.bind(this); - this.setupMfa = this.setupMfa.bind(this); - this.removeMfa = this.removeMfa.bind(this); - this.updateCurrentPassword = this.updateCurrentPassword.bind(this); - this.updateNewPassword = this.updateNewPassword.bind(this); - this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - this.getDefaultState = this.getDefaultState.bind(this); - this.createPasswordSection = this.createPasswordSection.bind(this); - this.createSignInSection = this.createSignInSection.bind(this); - this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this); - this.deauthorizeApp = this.deauthorizeApp.bind(this); - - this.state = this.getDefaultState(); - } - - getDefaultState() { - return { - currentPassword: '', - newPassword: '', - confirmPassword: '', - passwordError: '', - serverError: '', - authService: this.props.user.auth_service - }; - } - - componentDidMount() { - if (global.mm_config.EnableOAuthServiceProvider === 'true') { - getAuthorizedApps( - (authorizedApps) => { - this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state - }, - (err) => { - this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state - }); - } - } - - submitPassword(e) { - e.preventDefault(); - - var user = this.props.user; - var currentPassword = this.state.currentPassword; - var newPassword = this.state.newPassword; - var confirmPassword = this.state.confirmPassword; - - if (currentPassword === '') { - this.setState({passwordError: Utils.localizeMessage('user.settings.security.currentPasswordError', 'Please enter your current password.'), serverError: ''}); - return; - } - - const passwordErr = Utils.isValidPassword(newPassword); - if (passwordErr !== '') { - this.setState({ - passwordError: passwordErr, - serverError: '' - }); - return; - } - - if (newPassword !== confirmPassword) { - var defaultState = Object.assign(this.getDefaultState(), {passwordError: Utils.localizeMessage('user.settings.security.passwordMatchError', 'The new passwords you entered do not match.'), serverError: ''}); - this.setState(defaultState); - return; - } - - updatePassword( - user.id, - currentPassword, - newPassword, - () => { - this.props.updateSection(''); - AsyncClient.getMe(); - this.setState(this.getDefaultState()); - }, - (err) => { - var state = this.getDefaultState(); - if (err.message) { - state.serverError = err.message; - } else { - state.serverError = err; - } - state.passwordError = ''; - this.setState(state); - } - ); - } - - setupMfa(e) { - e.preventDefault(); - browserHistory.push('/mfa/setup'); - } - - removeMfa() { - deactivateMfa( - () => { - if (global.window.mm_license.MFA === 'true' && - global.window.mm_config.EnableMultifactorAuthentication === 'true' && - global.window.mm_config.EnforceMultifactorAuthentication === 'true') { - window.location.href = '/mfa/setup'; - return; - } - - this.props.updateSection(''); - this.setState(this.getDefaultState()); - }, - (err) => { - const state = this.getDefaultState(); - if (err.message) { - state.serverError = err.message; - } else { - state.serverError = err; - } - this.setState(state); - } - ); - } - - updateCurrentPassword(e) { - this.setState({currentPassword: e.target.value}); - } - - updateNewPassword(e) { - this.setState({newPassword: e.target.value}); - } - - updateConfirmPassword(e) { - this.setState({confirmPassword: e.target.value}); - } - - deauthorizeApp(e) { - e.preventDefault(); - const appId = e.currentTarget.getAttribute('data-app'); - deauthorizeOAuthApp( - appId, - () => { - const authorizedApps = this.state.authorizedApps.filter((app) => { - return app.id !== appId; - }); - - this.setState({authorizedApps, serverError: null}); - }, - (err) => { - this.setState({serverError: err.message}); - }); - } - - createMfaSection() { - let updateSectionStatus; - let submit; - - if (this.props.activeSection === 'mfa') { - let content; - let extraInfo; - if (this.props.user.mfa_active) { - let mfaRemoveHelp; - let mfaButtonText; - - if (global.window.mm_config.EnforceMultifactorAuthentication === 'true') { - mfaRemoveHelp = ( - - ); - - mfaButtonText = ( - - ); - } else { - mfaRemoveHelp = ( - - ); - - mfaButtonText = ( - - ); - } - - content = ( - - ); - - extraInfo = ( - - {mfaRemoveHelp} - - ); - } else { - content = ( -
    - - - -
    -
    - ); - - extraInfo = ( - - - - ); - } - - const inputs = []; - inputs.push( -
    - {content} -
    - ); - - updateSectionStatus = function resetSection(e) { - this.props.updateSection(''); - this.setState({serverError: null}); - e.preventDefault(); - }.bind(this); - - return ( - - ); - } - - let describe; - if (this.props.user.mfa_active) { - describe = Utils.localizeMessage('user.settings.security.active', 'Active'); - } else { - describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive'); - } - - updateSectionStatus = function updateSection() { - this.props.updateSection('mfa'); - }.bind(this); - - return ( - - ); - } - - createPasswordSection() { - let updateSectionStatus; - - if (this.props.activeSection === 'password') { - const inputs = []; - let submit; - - if (this.props.user.auth_service === '') { - submit = this.submitPassword; - - inputs.push( -
    - -
    - -
    -
    - ); - inputs.push( -
    - -
    - -
    -
    - ); - inputs.push( -
    - -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { - inputs.push( -
    -
    - -
    -
    - ); - } - - updateSectionStatus = function resetSection(e) { - this.props.updateSection(''); - this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - e.preventDefault(); - $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); - }.bind(this); - - return ( - - } - inputs={inputs} - submit={submit} - server_error={this.state.serverError} - client_error={this.state.passwordError} - updateSection={updateSectionStatus} - /> - ); - } - - let describe; - - if (this.props.user.auth_service === '') { - const d = new Date(this.props.user.last_password_update); - const hours12 = !PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Constants.Preferences.USE_MILITARY_TIME, false); - - describe = ( - - ), - time: ( - - ) - }} - /> - ); - } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { - describe = ( - - ); - } - - updateSectionStatus = function updateSection() { - this.props.updateSection('password'); - }.bind(this); - - return ( - - } - describe={describe} - updateSection={updateSectionStatus} - /> - ); - } - - createSignInSection() { - let updateSectionStatus; - const user = this.props.user; - - if (this.props.activeSection === 'signin') { - let emailOption; - let gitlabOption; - let googleOption; - let office365Option; - let ldapOption; - let samlOption; - - if (user.auth_service === '') { - if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { - gitlabOption = ( -
    - - -
    -
    - ); - } - - if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { - googleOption = ( -
    - - -
    -
    - ); - } - - if (global.window.mm_config.EnableSignUpWithOffice365 === 'true') { - office365Option = ( -
    - - -
    -
    - ); - } - - if (global.window.mm_config.EnableLdap === 'true') { - ldapOption = ( -
    - - - -
    -
    - ); - } - - if (global.window.mm_config.EnableSaml === 'true') { - samlOption = ( -
    - - -
    -
    - ); - } - } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') { - let link; - if (user.auth_service === Constants.LDAP_SERVICE) { - link = '/claim/ldap_to_email?email=' + encodeURIComponent(user.email); - } else { - link = '/claim/oauth_to_email?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service; - } - - emailOption = ( -
    - - - -
    -
    - ); - } - - const inputs = []; - inputs.push( -
    - {emailOption} - {gitlabOption} - {googleOption} - {office365Option} - {ldapOption} - {samlOption} -
    - ); - - updateSectionStatus = function updateSection(e) { - this.props.updateSection(''); - this.setState({serverError: null}); - e.preventDefault(); - }.bind(this); - - const extraInfo = ( - - - - ); - - return ( - - ); - } - - updateSectionStatus = function updateSection() { - this.props.updateSection('signin'); - }.bind(this); - - let describe = ( - - ); - if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { - describe = ( - - ); - } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { - describe = ( - - ); - } - - return ( - - ); - } - - createOAuthAppsSection() { - let updateSectionStatus; - - if (this.props.activeSection === 'apps') { - let apps; - if (this.state.authorizedApps && this.state.authorizedApps.length > 0) { - apps = this.state.authorizedApps.map((app) => { - const homepage = ( - - {app.homepage} - - ); - - return ( -
    -
    -
    - {app.name} - - {' -'} {homepage} - -
    -
    {app.description}
    -
    - - - -
    -
    -
    - {app.name} -
    -
    -
    - ); - }); - } else { - apps = ( -
    -
    - -
    -
    - ); - } - - const inputs = []; - let wrapperClass; - let helpText; - if (Array.isArray(apps)) { - wrapperClass = 'authorized-apps__wrapper'; - - helpText = ( -
    - -
    - ); - } - - inputs.push( -
    - {apps} -
    - ); - - updateSectionStatus = function updateSection(e) { - this.props.updateSection(''); - this.setState({serverError: null}); - e.preventDefault(); - }.bind(this); - - const title = ( -
    - - {helpText} -
    - ); - - return ( - - ); - } - - updateSectionStatus = function updateSection() { - this.props.updateSection('apps'); - }.bind(this); - - return ( - - } - updateSection={updateSectionStatus} - /> - ); - } - - render() { - const user = this.props.user; - const config = window.mm_config; - - const passwordSection = this.createPasswordSection(); - - let numMethods = 0; - numMethods = config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; - numMethods = config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; - numMethods = config.EnableLdap === 'true' ? numMethods + 1 : numMethods; - numMethods = config.EnableSaml === 'true' ? numMethods + 1 : numMethods; - - // If there are other sign-in methods and either email is enabled or the user's account is email, then allow switching - let signInSection; - if ((config.EnableSignUpWithEmail === 'true' || user.auth_service === '') && numMethods > 0) { - signInSection = this.createSignInSection(); - } - - let mfaSection; - if (config.EnableMultifactorAuthentication === 'true' && - global.window.mm_license.IsLicensed === 'true' && - (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) { - mfaSection = this.createMfaSection(); - } - - let oauthSection; - if (config.EnableOAuthServiceProvider === 'true') { - oauthSection = this.createOAuthAppsSection(); - } - - return ( -
    -
    - -

    -
    - -
    - -

    -
    -
    -

    - -

    -
    - {passwordSection} -
    - {mfaSection} -
    - {oauthSection} -
    - {signInSection} -
    -
    - - - - - - - - - -
    -
    - ); - } -} - -SecurityTab.defaultProps = { - user: {}, - activeSection: '' -}; -SecurityTab.propTypes = { - user: React.PropTypes.object, - activeSection: React.PropTypes.string, - updateSection: React.PropTypes.func, - updateTab: React.PropTypes.func, - closeModal: React.PropTypes.func.isRequired, - collapseModal: React.PropTypes.func.isRequired, - setEnforceFocus: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/user_settings/user_settings_security/index.js b/webapp/components/user_settings/user_settings_security/index.js new file mode 100644 index 000000000..cdbabd055 --- /dev/null +++ b/webapp/components/user_settings/user_settings_security/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getMe} from 'mattermost-redux/actions/users'; + +import SecurityTab from './user_settings_security.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getMe + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(SecurityTab); diff --git a/webapp/components/user_settings/user_settings_security/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx new file mode 100644 index 000000000..d4a372454 --- /dev/null +++ b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx @@ -0,0 +1,1036 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import SettingItemMin from 'components/setting_item_min.jsx'; +import SettingItemMax from 'components/setting_item_max.jsx'; +import AccessHistoryModal from 'components/access_history_modal'; +import ActivityLogModal from 'components/activity_log_modal'; +import ToggleModalButton from 'components/toggle_modal_button.jsx'; + +import PreferenceStore from 'stores/preference_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; + +import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx'; + +import $ from 'jquery'; +import React from 'react'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; +import {browserHistory, Link} from 'react-router/es6'; + +import icon50 from 'images/icon50x50.png'; + +export default class SecurityTab extends React.Component { + static propTypes = { + user: React.PropTypes.object, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired, + setEnforceFocus: React.PropTypes.func.isRequired, + actions: React.PropTypes.shape({ + getMe: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.submitPassword = this.submitPassword.bind(this); + this.setupMfa = this.setupMfa.bind(this); + this.removeMfa = this.removeMfa.bind(this); + this.updateCurrentPassword = this.updateCurrentPassword.bind(this); + this.updateNewPassword = this.updateNewPassword.bind(this); + this.updateConfirmPassword = this.updateConfirmPassword.bind(this); + this.getDefaultState = this.getDefaultState.bind(this); + this.createPasswordSection = this.createPasswordSection.bind(this); + this.createSignInSection = this.createSignInSection.bind(this); + this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this); + this.deauthorizeApp = this.deauthorizeApp.bind(this); + + this.state = this.getDefaultState(); + } + + getDefaultState() { + return { + currentPassword: '', + newPassword: '', + confirmPassword: '', + passwordError: '', + serverError: '', + authService: this.props.user.auth_service + }; + } + + componentDidMount() { + if (global.mm_config.EnableOAuthServiceProvider === 'true') { + getAuthorizedApps( + (authorizedApps) => { + this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state + }, + (err) => { + this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state + }); + } + } + + submitPassword(e) { + e.preventDefault(); + + var user = this.props.user; + var currentPassword = this.state.currentPassword; + var newPassword = this.state.newPassword; + var confirmPassword = this.state.confirmPassword; + + if (currentPassword === '') { + this.setState({passwordError: Utils.localizeMessage('user.settings.security.currentPasswordError', 'Please enter your current password.'), serverError: ''}); + return; + } + + const passwordErr = Utils.isValidPassword(newPassword); + if (passwordErr !== '') { + this.setState({ + passwordError: passwordErr, + serverError: '' + }); + return; + } + + if (newPassword !== confirmPassword) { + var defaultState = Object.assign(this.getDefaultState(), {passwordError: Utils.localizeMessage('user.settings.security.passwordMatchError', 'The new passwords you entered do not match.'), serverError: ''}); + this.setState(defaultState); + return; + } + + updatePassword( + user.id, + currentPassword, + newPassword, + () => { + this.props.updateSection(''); + this.props.actions.getMe(); + this.setState(this.getDefaultState()); + }, + (err) => { + var state = this.getDefaultState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + state.passwordError = ''; + this.setState(state); + } + ); + } + + setupMfa(e) { + e.preventDefault(); + browserHistory.push('/mfa/setup'); + } + + removeMfa() { + deactivateMfa( + () => { + if (global.window.mm_license.MFA === 'true' && + global.window.mm_config.EnableMultifactorAuthentication === 'true' && + global.window.mm_config.EnforceMultifactorAuthentication === 'true') { + window.location.href = '/mfa/setup'; + return; + } + + this.props.updateSection(''); + this.setState(this.getDefaultState()); + }, + (err) => { + const state = this.getDefaultState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + this.setState(state); + } + ); + } + + updateCurrentPassword(e) { + this.setState({currentPassword: e.target.value}); + } + + updateNewPassword(e) { + this.setState({newPassword: e.target.value}); + } + + updateConfirmPassword(e) { + this.setState({confirmPassword: e.target.value}); + } + + deauthorizeApp(e) { + e.preventDefault(); + const appId = e.currentTarget.getAttribute('data-app'); + deauthorizeOAuthApp( + appId, + () => { + const authorizedApps = this.state.authorizedApps.filter((app) => { + return app.id !== appId; + }); + + this.setState({authorizedApps, serverError: null}); + }, + (err) => { + this.setState({serverError: err.message}); + }); + } + + createMfaSection() { + let updateSectionStatus; + let submit; + + if (this.props.activeSection === 'mfa') { + let content; + let extraInfo; + if (this.props.user.mfa_active) { + let mfaRemoveHelp; + let mfaButtonText; + + if (global.window.mm_config.EnforceMultifactorAuthentication === 'true') { + mfaRemoveHelp = ( + + ); + + mfaButtonText = ( + + ); + } else { + mfaRemoveHelp = ( + + ); + + mfaButtonText = ( + + ); + } + + content = ( + + ); + + extraInfo = ( + + {mfaRemoveHelp} + + ); + } else { + content = ( +
    + + + +
    +
    + ); + + extraInfo = ( + + + + ); + } + + const inputs = []; + inputs.push( +
    + {content} +
    + ); + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); + }.bind(this); + + return ( + + ); + } + + let describe; + if (this.props.user.mfa_active) { + describe = Utils.localizeMessage('user.settings.security.active', 'Active'); + } else { + describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive'); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('mfa'); + }.bind(this); + + return ( + + ); + } + + createPasswordSection() { + let updateSectionStatus; + + if (this.props.activeSection === 'password') { + const inputs = []; + let submit; + + if (this.props.user.auth_service === '') { + submit = this.submitPassword; + + inputs.push( +
    + +
    + +
    +
    + ); + inputs.push( +
    + +
    + +
    +
    + ); + inputs.push( +
    + +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { + inputs.push( +
    +
    + +
    +
    + ); + } + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + e.preventDefault(); + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + }.bind(this); + + return ( + + } + inputs={inputs} + submit={submit} + server_error={this.state.serverError} + client_error={this.state.passwordError} + updateSection={updateSectionStatus} + /> + ); + } + + let describe; + + if (this.props.user.auth_service === '') { + const d = new Date(this.props.user.last_password_update); + const hours12 = !PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Constants.Preferences.USE_MILITARY_TIME, false); + + describe = ( + + ), + time: ( + + ) + }} + /> + ); + } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { + describe = ( + + ); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); + + return ( + + } + describe={describe} + updateSection={updateSectionStatus} + /> + ); + } + + createSignInSection() { + let updateSectionStatus; + const user = this.props.user; + + if (this.props.activeSection === 'signin') { + let emailOption; + let gitlabOption; + let googleOption; + let office365Option; + let ldapOption; + let samlOption; + + if (user.auth_service === '') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { + gitlabOption = ( +
    + + +
    +
    + ); + } + + if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { + googleOption = ( +
    + + +
    +
    + ); + } + + if (global.window.mm_config.EnableSignUpWithOffice365 === 'true') { + office365Option = ( +
    + + +
    +
    + ); + } + + if (global.window.mm_config.EnableLdap === 'true') { + ldapOption = ( +
    + + + +
    +
    + ); + } + + if (global.window.mm_config.EnableSaml === 'true') { + samlOption = ( +
    + + +
    +
    + ); + } + } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') { + let link; + if (user.auth_service === Constants.LDAP_SERVICE) { + link = '/claim/ldap_to_email?email=' + encodeURIComponent(user.email); + } else { + link = '/claim/oauth_to_email?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service; + } + + emailOption = ( +
    + + + +
    +
    + ); + } + + const inputs = []; + inputs.push( +
    + {emailOption} + {gitlabOption} + {googleOption} + {office365Option} + {ldapOption} + {samlOption} +
    + ); + + updateSectionStatus = function updateSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); + }.bind(this); + + const extraInfo = ( + + + + ); + + return ( + + ); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('signin'); + }.bind(this); + + let describe = ( + + ); + if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) { + describe = ( + + ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + + ); + } + + return ( + + ); + } + + createOAuthAppsSection() { + let updateSectionStatus; + + if (this.props.activeSection === 'apps') { + let apps; + if (this.state.authorizedApps && this.state.authorizedApps.length > 0) { + apps = this.state.authorizedApps.map((app) => { + const homepage = ( + + {app.homepage} + + ); + + return ( +
    +
    +
    + {app.name} + + {' -'} {homepage} + +
    +
    {app.description}
    +
    + + + +
    +
    +
    + {app.name} +
    +
    +
    + ); + }); + } else { + apps = ( +
    +
    + +
    +
    + ); + } + + const inputs = []; + let wrapperClass; + let helpText; + if (Array.isArray(apps)) { + wrapperClass = 'authorized-apps__wrapper'; + + helpText = ( +
    + +
    + ); + } + + inputs.push( +
    + {apps} +
    + ); + + updateSectionStatus = function updateSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); + }.bind(this); + + const title = ( +
    + + {helpText} +
    + ); + + return ( + + ); + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('apps'); + }.bind(this); + + return ( + + } + updateSection={updateSectionStatus} + /> + ); + } + + render() { + const user = this.props.user; + const config = window.mm_config; + + const passwordSection = this.createPasswordSection(); + + let numMethods = 0; + numMethods = config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableLdap === 'true' ? numMethods + 1 : numMethods; + numMethods = config.EnableSaml === 'true' ? numMethods + 1 : numMethods; + + // If there are other sign-in methods and either email is enabled or the user's account is email, then allow switching + let signInSection; + if ((config.EnableSignUpWithEmail === 'true' || user.auth_service === '') && numMethods > 0) { + signInSection = this.createSignInSection(); + } + + let mfaSection; + if (config.EnableMultifactorAuthentication === 'true' && + global.window.mm_license.IsLicensed === 'true' && + (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) { + mfaSection = this.createMfaSection(); + } + + let oauthSection; + if (config.EnableOAuthServiceProvider === 'true') { + oauthSection = this.createOAuthAppsSection(); + } + + return ( +
    +
    + +

    +
    + +
    + +

    +
    +
    +

    + +

    +
    + {passwordSection} +
    + {mfaSection} +
    + {oauthSection} +
    + {signInSection} +
    +
    + + + + + + + + + +
    +
    + ); + } +} + +SecurityTab.defaultProps = { + user: {}, + activeSection: '' +}; diff --git a/webapp/package.json b/webapp/package.json index b51eb4958..51f1a37ee 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -19,8 +19,10 @@ "intl": "1.2.5", "jasny-bootstrap": "3.1.3", "jquery": "3.1.1", + "localforage": "1.5.0", "marked": "mattermost/marked#8f5902fff9bad793cd6c66e0c44002c9e79e1317", "match-at": "0.1.0", + "mattermost-redux": "mattermost/mattermost-redux#webapp-part2", "object-assign": "4.1.1", "pdfjs-dist": "1.7.363", "perfect-scrollbar": "0.6.16", @@ -30,19 +32,26 @@ "react-custom-scrollbars": "4.0.2", "react-dom": "15.4.2", "react-intl": "2.2.3", + "react-redux": "5.0.4", "react-router": "2.8.1", "react-select": "1.0.0-rc.3", + "redux-batched-actions": "0.1.5", + "redux-persist": "4.6.0", + "redux-persist-transform-filter": "0.0.9", "superagent": "3.5.0", "twemoji": "2.2.5", "velocity-animate": "1.4.3", "webrtc-adapter": "3.2.0", + "whatwg-fetch": "2.0.3", "xregexp": "3.1.1" }, "devDependencies": { + "babel-cli": "6.24.1", "babel-core": "6.24.0", "babel-eslint": "7.1.1", "babel-jest": "19.0.0", "babel-loader": "6.4.0", + "babel-plugin-module-resolver": "2.7.0", "babel-plugin-transform-runtime": "6.23.0", "babel-polyfill": "6.23.0", "babel-preset-es2015": "6.24.0", @@ -73,9 +82,11 @@ "raw-loader": "0.5.1", "react-addons-test-utils": "15.4.2", "react-dom": "15.4.2", + "react-outside-event": "1.2.4", + "remote-redux-devtools": "0.5.7", + "remote-redux-devtools-on-debugger": "0.7.0", "sass-loader": "6.0.3", "style-loader": "0.13.2", - "react-outside-event": "1.2.4", "url-loader": "0.5.8", "webpack": "2.2.1", "webpack-node-externals": "1.5.4" diff --git a/webapp/root.jsx b/webapp/root.jsx index 177eb1ec4..b2da6a54c 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -6,22 +6,28 @@ require('perfect-scrollbar/jquery')($); import React from 'react'; import ReactDOM from 'react-dom'; +import {Provider} from 'react-redux'; import {Router, browserHistory} from 'react-router/es6'; import PDFJS from 'pdfjs-dist'; -import * as GlobalActions from 'actions/global_actions.jsx'; + import * as Websockets from 'actions/websocket_actions.jsx'; +import {loadMeAndConfig} from 'actions/user_actions.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as I18n from 'i18n/i18n.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; +import {getClientConfig, getLicenseConfig, setUrl} from 'mattermost-redux/actions/general'; + // Import our styles import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css'; import 'google-fonts/google-fonts.css'; import 'sass/styles.scss'; import 'katex/dist/katex.min.css'; +import store from 'stores/redux_store.jsx'; + // Import the root of our routing tree import rRoot from 'routes/route_root.jsx'; @@ -51,11 +57,26 @@ function preRenderSetup(callwhendone) { var d1 = $.Deferred(); //eslint-disable-line new-cap - GlobalActions.emitInitialLoad( - () => { - d1.resolve(); - } - ); + setUrl(window.location.origin); + + const currentUserId = localStorage.getItem('currentUserId'); + + if (currentUserId) { + loadMeAndConfig(() => d1.resolve()); + } else { + getClientConfig()(store.dispatch, store.getState).then( + (config) => { + global.window.mm_config = config; + + getLicenseConfig()(store.dispatch, store.getState).then( + (license) => { + global.window.mm_license = license; + d1.resolve(); + } + ); + } + ); + } // Make sure the websockets close and reset version $(window).on('beforeunload', @@ -86,10 +107,12 @@ function preRenderSetup(callwhendone) { function renderRootComponent() { ReactDOM.render(( - + + + ), document.getElementById('root')); } diff --git a/webapp/store/index.js b/webapp/store/index.js new file mode 100644 index 000000000..1af7127e8 --- /dev/null +++ b/webapp/store/index.js @@ -0,0 +1,112 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {batchActions} from 'redux-batched-actions'; +import configureServiceStore from 'mattermost-redux/store'; +import {General, RequestStatus} from 'mattermost-redux/constants'; +import reduxInitialState from 'mattermost-redux/store/initial_state'; +import {createTransform, persistStore} from 'redux-persist'; +import localForage from 'localforage'; + +import {transformSet} from './utils'; + +const usersSetTransform = [ + 'profilesInChannel', + 'profilesNotInChannel', + 'profilesInTeam', + 'profilesNotInTeam' +]; + +const teamSetTransform = [ + 'membersInTeam' +]; + +const setTransforms = [ + ...usersSetTransform, + ...teamSetTransform +]; + +export default function configureStore(initialState) { + const setTransformer = createTransform( + (inboundState, key) => { + if (key === 'entities') { + const state = {...inboundState}; + for (const prop in state) { + if (state.hasOwnProperty(prop)) { + state[prop] = transformSet(state[prop], setTransforms); + } + } + + return state; + } + + return inboundState; + }, + (outboundState, key) => { + if (key === 'entities') { + const state = {...outboundState}; + for (const prop in state) { + if (state.hasOwnProperty(prop)) { + state[prop] = transformSet(state[prop], setTransforms, false); + } + } + + return state; + } + + return outboundState; + } + ); + + const offlineOptions = { + persist: (store, options) => { + const persistor = persistStore(store, {storage: localForage, ...options}, () => { + store.dispatch({ + type: General.STORE_REHYDRATION_COMPLETE, + complete: true + }); + }); + + let purging = false; + + // check to see if the logout request was successful + store.subscribe(() => { + const state = store.getState(); + if (state.requests.users.logout.status === RequestStatus.SUCCESS && !purging) { + purging = true; + + persistor.purge(); + + store.dispatch(batchActions([ + { + type: General.OFFLINE_STORE_RESET, + data: Object.assign({}, reduxInitialState, initialState) + } + ])); + + localStorage.removeItem('currentUserId'); + window.location.href = '/'; + + setTimeout(() => { + purging = false; + }, 500); + } + }); + + return persistor; + }, + persistOptions: { + autoRehydrate: { + log: false + }, + blacklist: ['errors', 'offline', 'requests', 'entities'], + debounce: 500, + transforms: [ + setTransformer + ] + } + }; + + return configureServiceStore({}, {}, offlineOptions); +} + diff --git a/webapp/store/utils.js b/webapp/store/utils.js new file mode 100644 index 000000000..5566f54b8 --- /dev/null +++ b/webapp/store/utils.js @@ -0,0 +1,42 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +function transformFromSet(incoming) { + const state = {...incoming}; + + for (const key in state) { + if (state.hasOwnProperty(key)) { + if (state[key] instanceof Set) { + state[key] = Array.from([...state[key]]); + } + } + } + + return state; +} + +function transformToSet(incoming) { + const state = {...incoming}; + + for (const key in state) { + if (state.hasOwnProperty(key)) { + state[key] = new Set(state[key]); + } + } + + return state; +} + +export function transformSet(incoming, setTransforms, toStorage = true) { + const state = {...incoming}; + + const transformer = toStorage ? transformFromSet : transformToSet; + + for (const key in state) { + if (state.hasOwnProperty(key) && setTransforms.includes(key)) { + state[key] = transformer(state[key]); + } + } + + return state; +} diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx index 7a286f7a2..f3476d9ea 100644 --- a/webapp/stores/preference_store.jsx +++ b/webapp/stores/preference_store.jsx @@ -8,6 +8,10 @@ import EventEmitter from 'events'; const CHANGE_EVENT = 'change'; +import store from 'stores/redux_store.jsx'; +import * as Selectors from 'mattermost-redux/selectors/entities/preferences'; +import {PreferenceTypes} from 'mattermost-redux/action_types'; + class PreferenceStore extends EventEmitter { constructor() { super(); @@ -16,6 +20,23 @@ class PreferenceStore extends EventEmitter { this.dispatchToken = AppDispatcher.register(this.handleEventPayload); this.preferences = new Map(); + this.entities = Selectors.getMyPreferences(store.getState()); + Object.keys(this.entities).forEach((key) => { + this.preferences.set(key, this.entities[key].value); + }); + + store.subscribe(() => { + const newEntities = Selectors.getMyPreferences(store.getState()); + if (this.entities !== newEntities) { + this.preferences = new Map(); + Object.keys(newEntities).forEach((key) => { + this.preferences.set(key, newEntities[key].value); + }); + this.emitChange(); + } + + this.entities = newEntities; + }); this.setMaxListeners(600); } @@ -79,21 +100,24 @@ class PreferenceStore extends EventEmitter { } setPreference(category, name, value) { - this.preferences.set(this.getKey(category, name), value); + store.dispatch({ + type: PreferenceTypes.RECEIVED_PREFERENCES, + data: [{category, name, value}] + }); } setPreferencesFromServer(newPreferences) { - for (const preference of newPreferences) { - this.setPreference(preference.category, preference.name, preference.value); - } + store.dispatch({ + type: PreferenceTypes.RECEIVED_PREFERENCES, + data: newPreferences + }); } deletePreference(preference) { - this.preferences.delete(this.getKey(preference.category, preference.name)); - } - - clear() { - this.preferences.clear(); + store.dispatch({ + type: PreferenceTypes.DELETED_PREFERENCES, + data: [preference] + }); } emitChange(category) { diff --git a/webapp/stores/redux_store.jsx b/webapp/stores/redux_store.jsx new file mode 100644 index 000000000..de5099d27 --- /dev/null +++ b/webapp/stores/redux_store.jsx @@ -0,0 +1,19 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// This is a temporary store while we are transitioning from Flux to Redux. This file exports +// the configured Redux store for use by actions and selectors. + +import configureStore from 'store'; +const store = configureStore(); + +export function bindActionToRedux(action, ...args) { + return async () => { + await action(...args)(store.dispatch, store.getState); + }; +} + +window.store = store; + +export default store; + diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index 85480bdac..1d3d5ff25 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -17,17 +17,33 @@ const CHANGE_EVENT = 'change'; const STATS_EVENT = 'stats'; const UNREAD_EVENT = 'unread'; +import store from 'stores/redux_store.jsx'; +import * as Selectors from 'mattermost-redux/selectors/entities/teams'; +import {TeamTypes} from 'mattermost-redux/action_types'; + var Utils; class TeamStoreClass extends EventEmitter { constructor() { super(); this.clear(); + + store.subscribe(() => { + const newEntities = store.getState().entities.teams; + + if (newEntities.teams !== this.entities.teams) { + this.emitChange(); + } + if (newEntities.myMembers !== this.entities.myMembers) { + this.emitChange(); + } + + this.entities = newEntities; + }); } clear() { - this.teams = {}; - this.my_team_members = []; + this.entities = {}; this.members_in_team = {}; this.members_not_in_team = {}; this.stats = {}; @@ -91,7 +107,7 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return this.teams; + return store.getState().entities.teams.teams; } getCurrentId() { @@ -100,10 +116,14 @@ class TeamStoreClass extends EventEmitter { setCurrentId(id) { this.currentTeamId = id; + store.dispatch({ + type: TeamTypes.SELECT_TEAM, + data: id + }); } getCurrent() { - const team = this.teams[this.currentTeamId]; + const team = this.getAll()[this.currentTeamId]; if (team) { return team; @@ -165,17 +185,21 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - this.teams[team.id] = team; + this.saveTeams([team]); } saveTeams(teams) { - this.teams = teams; + store.dispatch({ + type: TeamTypes.RECEIVED_TEAMS_LIST, + data: teams + }); } updateTeam(team) { const t = JSON.parse(team); - if (this.teams && this.teams[t.id]) { - this.teams[t.id] = t; + const teams = this.getAll(); + if (teams && teams[t.id]) { + this.saveTeam(t); } if (this.teamListings && this.teamListings[t.id]) { @@ -193,7 +217,7 @@ class TeamStoreClass extends EventEmitter { saveMyTeam(team) { this.saveTeam(team); - this.currentTeamId = team.id; + this.setCurrentId(team.id); } saveStats(teamId, stats) { @@ -201,20 +225,26 @@ class TeamStoreClass extends EventEmitter { } saveMyTeamMembers(members) { - this.my_team_members = members; + store.dispatch({ + type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS, + data: members + }); } appendMyTeamMember(member) { - this.my_team_members.push(member); + const members = this.getMyTeamMembers(); + members.push(member); + this.saveMyTeamMembers(members); } saveMyTeamMembersUnread(members) { - for (let i = 0; i < this.my_team_members.length; i++) { - const team = this.my_team_members[i]; + const myMembers = this.getMyTeamMembers(); + for (let i = 0; i < myMembers.length; i++) { + const team = myMembers[i]; const member = members.filter((m) => m.team_id === team.team_id)[0]; if (member) { - this.my_team_members[i] = Object.assign({}, + myMembers[i] = Object.assign({}, team, { msg_count: member.msg_count, @@ -222,19 +252,23 @@ class TeamStoreClass extends EventEmitter { }); } } + + this.saveMyTeamMembers(myMembers); } removeMyTeamMember(teamId) { - for (let i = 0; i < this.my_team_members.length; i++) { - if (this.my_team_members[i].team_id === teamId) { - this.my_team_members.splice(i, 1); + const myMembers = this.getMyTeamMembers(); + for (let i = 0; i < myMembers.length; i++) { + if (myMembers[i].team_id === teamId) { + myMembers.splice(i, 1); } } - this.emitChange(); + + this.saveMyTeamMembers(myMembers); } getMyTeamMembers() { - return this.my_team_members; + return Object.values(Selectors.getTeamMemberships(store.getState())); } saveMembersInTeam(teamId = this.getCurrentId(), members) { @@ -320,19 +354,21 @@ class TeamStoreClass extends EventEmitter { } updateUnreadCount(teamId, totalMsgCount, channelMember) { - const member = this.my_team_members.filter((m) => m.team_id === teamId)[0]; + let member = this.getMyTeamMembers().filter((m) => m.team_id === teamId)[0]; if (member) { + member = Object.assign({}, member); member.msg_count -= (totalMsgCount - channelMember.msg_count); member.mention_count -= channelMember.mention_count; } } subtractUnread(teamId, msgs, mentions) { - const member = this.my_team_members.filter((m) => m.team_id === teamId)[0]; + let member = this.getMyTeamMembers().filter((m) => m.team_id === teamId)[0]; if (member) { const msgCount = member.msg_count - msgs; const mentionCount = member.mention_count - mentions; + member = Object.assign({}, member); member.msg_count = (msgCount > 0) ? msgCount : 0; member.mention_count = (mentionCount > 0) ? mentionCount : 0; } @@ -344,7 +380,7 @@ class TeamStoreClass extends EventEmitter { return; } - const member = this.my_team_members.filter((m) => m.team_id === id)[0]; + const member = Object.assign({}, this.getMyTeamMembers().filter((m) => m.team_id === id)[0]); member.msg_count++; } @@ -355,7 +391,7 @@ class TeamStoreClass extends EventEmitter { } if (mentions.indexOf(UserStore.getCurrentId()) !== -1) { - const member = this.my_team_members.filter((m) => m.team_id === id)[0]; + const member = Object.assign({}, this.getMyTeamMembers().filter((m) => m.team_id === id)[0]); member.mention_count++; } } diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index fa077f16b..a99c4b37a 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -1,16 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import EventEmitter from 'events'; -import * as GlobalActions from 'actions/global_actions.jsx'; -import LocalizationStore from './localization_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; -const ActionTypes = Constants.ActionTypes; const UserStatuses = Constants.UserStatuses; const CHANGE_EVENT_NOT_IN_CHANNEL = 'change_not_in_channel'; @@ -23,48 +19,52 @@ const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; const CHANGE_EVENT_STATUSES = 'change_statuses'; +import store from 'stores/redux_store.jsx'; +import * as Selectors from 'mattermost-redux/selectors/entities/users'; +import {UserTypes} from 'mattermost-redux/action_types'; + var Utils; class UserStoreClass extends EventEmitter { constructor() { super(); - this.clear(); - } - - clear() { - // All the profiles, regardless of where they came from - this.profiles = {}; - this.paging_offset = 0; - this.paging_count = 0; - - // Lists of sorted IDs for users in a team - this.profiles_not_in_team = {}; - this.not_in_team_offset = 0; - this.not_in_team_count = 0; - - // Lists of sorted IDs for users in a team - this.profiles_in_team = {}; - this.in_team_offset = 0; - this.in_team_count = 0; - - // Lists of sorted IDs for users in a channel - this.profiles_in_channel = {}; - this.in_channel_offset = {}; - this.in_channel_count = {}; - - // Lists of sorted IDs for users not in a channel - this.profiles_not_in_channel = {}; - this.not_in_channel_offset = {}; - this.not_in_channel_count = {}; - - // Lists of sorted IDs for users without a team - this.profiles_without_team = {}; - - this.statuses = {}; - this.sessions = {}; - this.audits = []; - this.currentUserId = ''; + this.noAccounts = false; + this.entities = {}; + + store.subscribe(() => { + const newEntities = store.getState().entities.users; + + if (newEntities.profiles !== this.entities.profiles) { + this.emitChange(); + } + if (newEntities.profilesInChannel !== this.entities.profilesInChannel) { + this.emitInChannelChange(); + } + if (newEntities.profilesNotInChannel !== this.entities.profilesNotInChannel) { + this.emitNotInChannelChange(); + } + if (newEntities.profilesInTeam !== this.entities.profilesInTeam) { + this.emitInTeamChange(); + } + if (newEntities.profilesNotInTeam !== this.entities.profilesNotInTeam) { + this.emitNotInTeamChange(); + } + if (newEntities.profilesWithoutTeam !== this.entities.profilesWithoutTeam) { + this.emitWithoutTeamChange(); + } + if (newEntities.statuses !== this.entities.statuses) { + this.emitStatusesChange(); + } + if (newEntities.myAudits !== this.entities.myAudits) { + this.emitAuditsChange(); + } + if (newEntities.mySessions !== this.entities.mySessions) { + this.emitSessionsChange(); + } + + this.entities = newEntities; + }); } emitChange(userId) { @@ -178,49 +178,21 @@ class UserStoreClass extends EventEmitter { // General getCurrentUser() { - return this.getProfiles()[this.currentUserId]; - } - - setCurrentUser(user) { - this.saveProfile(user); - this.currentUserId = user.id; - global.window.mm_current_user_id = this.currentUserId; - if (LocalizationStore.getLocale() !== user.locale) { - setTimeout(() => GlobalActions.newLocalizationSelected(user.locale), 0); - } + return Selectors.getCurrentUser(store.getState()); } getCurrentId() { - var user = this.getCurrentUser(); - - if (user) { - return user.id; - } - - return null; + return Selectors.getCurrentUserId(store.getState()); } // System-Wide Profiles - saveProfiles(profiles) { - const newProfiles = Object.assign({}, profiles); - const currentId = this.getCurrentId(); - if (newProfiles[currentId]) { - Reflect.deleteProperty(newProfiles, currentId); - } - this.profiles = Object.assign({}, this.profiles, newProfiles); - } - getProfiles() { - return this.profiles; + return Selectors.getUsers(store.getState()); } getProfile(userId) { - if (this.profiles[userId]) { - return Object.assign({}, this.profiles[userId]); - } - - return null; + return Selectors.getUser(store.getState(), userId); } getProfileListForIds(userIds, skipCurrent = false, skipInactive = false) { @@ -257,17 +229,7 @@ class UserStoreClass extends EventEmitter { } getProfilesUsernameMap() { - var profileUsernameMap = {}; - - var profiles = this.getProfiles(); - for (var key in profiles) { - if (profiles.hasOwnProperty(key)) { - var profile = profiles[key]; - profileUsernameMap[profile.username] = profile; - } - } - - return profileUsernameMap; + return Selectors.getUsersByUsername(store.getState()); } getActiveOnlyProfiles(skipCurrent) { @@ -310,10 +272,11 @@ class UserStoreClass extends EventEmitter { getProfileList(skipCurrent = false, allowInactive = false) { const profiles = []; const currentId = this.getCurrentId(); + const profileMap = this.getProfiles(); - for (const id in this.profiles) { - if (this.profiles.hasOwnProperty(id)) { - var profile = this.profiles[id]; + for (const id in profileMap) { + if (profileMap.hasOwnProperty(id)) { + var profile = profileMap[id]; if (skipCurrent && id === currentId) { continue; @@ -339,103 +302,32 @@ class UserStoreClass extends EventEmitter { } saveProfile(profile) { - this.profiles[profile.id] = profile; + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE, + data: profile + }); } // Team-Wide Profiles - saveProfilesInTeam(teamId, profiles) { - const oldProfileList = this.profiles_in_team[teamId] || []; - const oldProfileMap = {}; - for (let i = 0; i < oldProfileList.length; i++) { - oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); - } - - const newProfileMap = Object.assign({}, oldProfileMap, profiles); - const newProfileList = Object.keys(newProfileMap); - - newProfileList.sort((a, b) => { - const aProfile = newProfileMap[a]; - const bProfile = newProfileMap[b]; - - if (aProfile.username < bProfile.username) { - return -1; - } - if (aProfile.username > bProfile.username) { - return 1; - } - return 0; - }); - - this.profiles_in_team[teamId] = newProfileList; - this.saveProfiles(profiles); - } - getProfileListInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) { - const userIds = this.profiles_in_team[teamId] || []; + const userIds = Array.from(Selectors.getUserIdsInTeams(store.getState())[teamId] || []); return this.getProfileListForIds(userIds, skipCurrent, skipInactive); } removeProfileFromTeam(teamId, userId) { - const userIds = this.profiles_in_team[teamId]; - if (!userIds) { - return; - } - - const index = userIds.indexOf(userId); - if (index === -1) { - return; - } - - userIds.splice(index, 1); - } - - // Not In Team Profiles - - saveProfilesNotInTeam(teamId, profiles) { - const oldProfileList = this.profiles_not_in_team[teamId] || []; - const oldProfileMap = {}; - for (let i = 0; i < oldProfileList.length; i++) { - oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); - } - - const newProfileMap = Object.assign({}, oldProfileMap, profiles); - const newProfileList = Object.keys(newProfileMap); - - newProfileList.sort((a, b) => { - const aProfile = newProfileMap[a]; - const bProfile = newProfileMap[b]; - - if (aProfile.username < bProfile.username) { - return -1; - } - if (aProfile.username > bProfile.username) { - return 1; - } - return 0; + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM, + data: {user_id: userId}, + id: teamId }); - - this.profiles_not_in_team[teamId] = newProfileList; - this.saveProfiles(profiles); } - removeProfileNotInTeam(teamId, userId) { - const userIds = this.profiles_not_in_team[teamId]; - if (!userIds) { - return; - } - - const index = userIds.indexOf(userId); - if (index === -1) { - return; - } - - userIds.splice(index, 1); - } + // Not In Team Profiles getProfileListNotInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) { - const userIds = this.profiles_not_in_team[teamId] || []; + const userIds = Array.from(Selectors.getUserIdsNotInTeams(store.getState())[teamId] || []); const profiles = []; const currentId = this.getCurrentId(); @@ -460,178 +352,84 @@ class UserStoreClass extends EventEmitter { return profiles; } - // Channel-Wide Profiles - - saveProfilesInChannel(channelId = ChannelStore.getCurrentId(), profiles) { - const oldProfileList = this.profiles_in_channel[channelId] || []; - const oldProfileMap = {}; - for (let i = 0; i < oldProfileList.length; i++) { - oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); - } - - const newProfileMap = Object.assign({}, oldProfileMap, profiles); - const newProfileList = Object.keys(newProfileMap); - - newProfileList.sort((a, b) => { - const aProfile = newProfileMap[a]; - const bProfile = newProfileMap[b]; - - if (aProfile.username < bProfile.username) { - return -1; - } - if (aProfile.username > bProfile.username) { - return 1; - } - return 0; + removeProfileNotInTeam(teamId, userId) { + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_IN_TEAM, + data: {user_id: userId}, + id: teamId }); - - this.profiles_in_channel[channelId] = newProfileList; - this.saveProfiles(profiles); } + // Channel-Wide Profiles + saveProfileInChannel(channelId = ChannelStore.getCurrentId(), profile) { - const profileMap = {}; - profileMap[profile.id] = profile; - this.saveProfilesInChannel(channelId, profileMap); + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: {user_id: profile.id}, + id: channelId + }); } saveUserIdInChannel(channelId = ChannelStore.getCurrentId(), userId) { - const profile = this.getProfile(userId); - - // Must have profile or we can't sort the list - if (!profile) { - return false; - } - - this.saveProfileInChannel(channelId, profile); - - return true; + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: {user_id: userId}, + id: channelId + }); } removeProfileInChannel(channelId, userId) { - const userIds = this.profiles_in_channel[channelId]; - if (!userIds) { - return; - } - - const index = userIds.indexOf(userId); - if (index === -1) { - return; - } - - userIds.splice(index, 1); + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: {user_id: userId}, + id: channelId + }); } getProfileListInChannel(channelId = ChannelStore.getCurrentId(), skipCurrent = false) { - const userIds = this.profiles_in_channel[channelId] || []; + const userIds = Array.from(Selectors.getUserIdsInChannels(store.getState())[channelId] || []); return this.getProfileListForIds(userIds, skipCurrent, false); } - saveProfilesNotInChannel(channelId = ChannelStore.getCurrentId(), profiles) { - const oldProfileList = this.profiles_not_in_channel[channelId] || []; - const oldProfileMap = {}; - for (let i = 0; i < oldProfileList.length; i++) { - oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); - } - - const newProfileMap = Object.assign({}, oldProfileMap, profiles); - const newProfileList = Object.keys(newProfileMap); - - newProfileList.sort((a, b) => { - const aProfile = newProfileMap[a]; - const bProfile = newProfileMap[b]; - - if (aProfile.username < bProfile.username) { - return -1; - } - if (aProfile.username > bProfile.username) { - return 1; - } - return 0; - }); - - this.profiles_not_in_channel[channelId] = newProfileList; - this.saveProfiles(profiles); - } - saveProfileNotInChannel(channelId = ChannelStore.getCurrentId(), profile) { - const profileMap = {}; - profileMap[profile.id] = profile; - this.saveProfilesNotInChannel(channelId, profileMap); + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, + data: {user_id: profile.id}, + id: channelId + }); } removeProfileNotInChannel(channelId, userId) { - const userIds = this.profiles_not_in_channel[channelId]; - if (!userIds) { - return; - } - - const index = userIds.indexOf(userId); - if (index === -1) { - return; - } - - userIds.splice(index, 1); + store.dispatch({ + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, + data: {user_id: userId}, + id: channelId + }); } getProfileListNotInChannel(channelId = ChannelStore.getCurrentId(), skipInactive = false) { - const userIds = this.profiles_not_in_channel[channelId] || []; + const userIds = Array.from(Selectors.getUserIdsNotInChannels(store.getState())[channelId] || []); return this.getProfileListForIds(userIds, false, skipInactive); } // Profiles without any teams - saveProfilesWithoutTeam(profiles) { - const oldProfileList = this.profiles_without_team; - const oldProfileMap = {}; - for (let i = 0; i < oldProfileList.length; i++) { - oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); - } - - const newProfileMap = Object.assign({}, oldProfileMap, profiles); - const newProfileList = Object.keys(newProfileMap); - - newProfileList.sort((a, b) => { - const aProfile = newProfileMap[a]; - const bProfile = newProfileMap[b]; - - if (aProfile.username < bProfile.username) { - return -1; - } - if (aProfile.username > bProfile.username) { - return 1; - } - return 0; - }); - - this.profiles_without_team = newProfileList; - this.saveProfiles(profiles); - } - getProfileListWithoutTeam(skipCurrent = false, skipInactive = false) { - const userIds = this.profiles_without_team || []; + const userIds = Array.from(Selectors.getUserIdsWithoutTeam(store.getState()) || []); return this.getProfileListForIds(userIds, skipCurrent, skipInactive); } // Other - setSessions(sessions) { - this.sessions = sessions; - } - getSessions() { - return this.sessions; - } - - setAudits(audits) { - this.audits = audits; + return store.getState().entities.users.mySessions; } getAudits() { - return this.audits; + return store.getState().entities.users.myAudits; } getCurrentMentionKeys() { @@ -668,17 +466,16 @@ class UserStoreClass extends EventEmitter { return keys; } - setStatuses(statuses) { - this.statuses = Object.assign(this.statuses, statuses); - } - setStatus(userId, status) { - this.statuses[userId] = status; - this.emitStatusesChange(); + const data = [{user_id: userId, status}]; + store.dispatch({ + type: UserTypes.RECEIVED_STATUSES, + data + }); } getStatuses() { - return this.statuses; + return store.getState().entities.users.statuses; } getStatus(id) { @@ -686,7 +483,7 @@ class UserStoreClass extends EventEmitter { } getNoAccounts() { - return this.noAccounts; + return global.window.mm_config.NoAccounts === 'true'; } setNoAccounts(noAccounts) { @@ -706,141 +503,9 @@ class UserStoreClass extends EventEmitter { return false; } - - setPage(offset, count) { - this.paging_offset = offset + count; - this.paging_count = this.paging_count + count; - } - - getPagingOffset() { - return this.paging_offset; - } - - getPagingCount() { - return this.paging_count; - } - - setInTeamPage(offset, count) { - this.in_team_offset = offset + count; - this.in_team_count = this.in_team_count + count; - } - - getInTeamPagingOffset() { - return this.in_team_offset; - } - - getInTeamPagingCount() { - return this.in_team_count; - } - - setNotInTeamPage(offset, count) { - this.not_in_team_offset = offset + count; - this.not_in_team_count = this.not_in_team_count + count; - } - - getNotInTeamPagingOffset() { - return this.not_in_team_offset; - } - - getNotInTeamPagingCount() { - return this.not_in_team_count; - } - - setInChannelPage(channelId, offset, count) { - this.in_channel_offset[channelId] = offset + count; - this.in_channel_count[channelId] = this.dm_paging_count + count; - } - - getInChannelPagingOffset(channelId) { - return this.in_channel_offset[channelId] | 0; - } - - getInChannelPagingCount(channelId) { - return this.in_channel_count[channelId] | 0; - } - - setNotInChannelPage(channelId, offset, count) { - this.not_in_channel_offset[channelId] = offset + count; - this.not_in_channel_count[channelId] = this.dm_paging_count + count; - } - - getNotInChannelPagingOffset(channelId) { - return this.not_in_channel_offset[channelId] | 0; - } - - getNotInChannelPagingCount(channelId) { - return this.not_in_channel_count[channelId] | 0; - } } var UserStore = new UserStoreClass(); UserStore.setMaxListeners(600); -UserStore.dispatchToken = AppDispatcher.register((payload) => { - var action = payload.action; - - switch (action.type) { - case ActionTypes.RECEIVED_PROFILES: - UserStore.saveProfiles(action.profiles); - if (action.offset != null && action.count != null) { - UserStore.setPage(action.offset, action.count); - } - UserStore.emitChange(); - break; - case ActionTypes.RECEIVED_PROFILES_IN_TEAM: - UserStore.saveProfilesInTeam(action.team_id, action.profiles); - if (action.offset != null && action.count != null) { - UserStore.setInTeamPage(action.offset, action.count); - } - UserStore.emitInTeamChange(); - break; - case ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM: - UserStore.saveProfilesNotInTeam(action.team_id, action.profiles); - if (action.offset != null && action.count != null) { - UserStore.setNotInTeamPage(action.offset, action.count); - } - UserStore.emitNotInTeamChange(); - break; - case ActionTypes.RECEIVED_PROFILES_IN_CHANNEL: - UserStore.saveProfilesInChannel(action.channel_id, action.profiles); - if (action.offset != null && action.count != null) { - UserStore.setInChannelPage(action.offset, action.count); - } - UserStore.emitInChannelChange(); - break; - case ActionTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL: - UserStore.saveProfilesNotInChannel(action.channel_id, action.profiles); - if (action.offset != null && action.count != null) { - UserStore.setNotInChannelPage(action.offset, action.count); - } - UserStore.emitNotInChannelChange(); - break; - case ActionTypes.RECEIVED_PROFILES_WITHOUT_TEAM: - UserStore.saveProfilesWithoutTeam(action.profiles); - UserStore.emitWithoutTeamChange(); - break; - case ActionTypes.RECEIVED_PROFILE: - UserStore.saveProfile(action.profile); - UserStore.emitChange(); - break; - case ActionTypes.RECEIVED_ME: - UserStore.setCurrentUser(action.me); - UserStore.emitChange(action.me.id); - break; - case ActionTypes.RECEIVED_SESSIONS: - UserStore.setSessions(action.sessions); - UserStore.emitSessionsChange(); - break; - case ActionTypes.RECEIVED_AUDITS: - UserStore.setAudits(action.audits); - UserStore.emitAuditsChange(); - break; - case ActionTypes.RECEIVED_STATUSES: - UserStore.setStatuses(action.statuses); - UserStore.emitStatusesChange(); - break; - default: - } -}); - export {UserStore as default}; diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index abc1017fa..cb911cb55 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -8,7 +8,6 @@ import TeamStore from 'stores/team_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {loadStatusesForProfilesMap} from 'actions/status_actions.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import Client from 'client/web_client.jsx'; @@ -323,231 +322,6 @@ export function getUser(userId, success, error) { ); } -export function getProfiles(offset = UserStore.getPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { - const callName = `getProfiles${offset}${limit}`; - - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfiles( - offset, - limit, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfiles'); - } - ); -} - -export function getProfilesInTeam(teamId = TeamStore.getCurrentId(), offset = UserStore.getInTeamPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { - const callName = `getProfilesInTeam${teamId}${offset}${limit}`; - - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfilesInTeam( - teamId, - offset, - limit, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_IN_TEAM, - profiles: data, - team_id: teamId, - offset, - count: Object.keys(data).length - }); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfilesInTeam'); - } - ); -} - -export function getProfilesNotInTeam(teamId = TeamStore.getCurrentId(), offset = UserStore.getInTeamPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { - const callName = `getProfilesNotInTeam${teamId}${offset}${limit}`; - - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfilesNotInTeam( - teamId, - offset, - limit, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM, - profiles: data, - team_id: teamId, - offset, - count: Object.keys(data).length - }); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfilesNotInTeam'); - } - ); -} - -export function getProfilesInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { - const callName = `getProfilesInChannel${channelId}${offset}${limit}`; - - if (isCallInProgress()) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfilesInChannel( - channelId, - offset, - limit, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_IN_CHANNEL, - channel_id: channelId, - profiles: data, - offset, - count: Object.keys(data).length - }); - - loadStatusesForProfilesMap(data); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfilesInChannel'); - } - ); -} - -export function getProfilesNotInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getNotInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { - const callName = `getProfilesNotInChannel${channelId}${offset}${limit}`; - - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfilesNotInChannel( - channelId, - offset, - limit, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, - channel_id: channelId, - profiles: data, - offset, - count: Object.keys(data).length - }); - - loadStatusesForProfilesMap(data); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfilesNotInChannel'); - } - ); -} - -export function getProfilesByIds(userIds) { - const callName = 'getProfilesByIds' + JSON.stringify(userIds); - - if (isCallInProgress(callName)) { - return; - } - - if (!userIds || userIds.length === 0) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - Client.getProfilesByIds( - userIds, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - }, - (err) => { - callTracker[callName] = 0; - dispatchError(err, 'getProfilesByIds'); - } - ); -} - -export function getSessions() { - if (isCallInProgress('getSessions')) { - return; - } - - callTracker.getSessions = utils.getTimestamp(); - Client.getSessions( - UserStore.getCurrentId(), - (data) => { - callTracker.getSessions = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SESSIONS, - sessions: data - }); - }, - (err) => { - callTracker.getSessions = 0; - dispatchError(err, 'getSessions'); - } - ); -} - -export function getAudits() { - if (isCallInProgress('getAudits')) { - return; - } - - callTracker.getAudits = utils.getTimestamp(); - Client.getAudits( - UserStore.getCurrentId(), - (data) => { - callTracker.getAudits = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_AUDITS, - audits: data - }); - }, - (err) => { - callTracker.getAudits = 0; - dispatchError(err, 'getAudits'); - } - ); -} - export function getLogs() { if (isCallInProgress('getLogs')) { return; diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index c6a3d7547..31ba8708d 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import * as Utils from './utils.jsx'; -import ChannelInviteModal from 'components/channel_invite_modal.jsx'; +import ChannelInviteModal from 'components/channel_invite_modal'; import EditChannelHeaderModal from 'components/edit_channel_header_modal.jsx'; import ToggleModalButton from 'components/toggle_modal_button.jsx'; import UserProfile from 'components/user_profile.jsx'; diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index 32c5a322a..40b16139b 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -23,7 +23,7 @@ if (NPM_TARGET === 'test') { } var config = { - entry: ['babel-polyfill', './root.jsx', 'root.html'], + entry: ['babel-polyfill', 'whatwg-fetch', './root.jsx', 'root.html'], output: { path: 'dist', publicPath: '/static/', @@ -33,7 +33,7 @@ var config = { module: { loaders: [ { - test: /\.jsx?$/, + test: /\.(js|jsx)?$/, loader: 'babel-loader', exclude: /(node_modules|non_npm_dependencies)/, query: { -- cgit v1.2.3-1-g7c22