* 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
Этот коммит содержится в:
Joram Wilander
2017-04-25 11:46:02 -04:00
коммит произвёл Christopher Speller
родитель cc07c00507
Коммит 6c4c706313
57 изменённых файлов: 1148 добавлений и 1454 удалений

Просмотреть файл

@@ -1,12 +1,11 @@
// 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 LoadingScreen from 'components/loading_screen.jsx';
import AuditTable from 'components/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';
@@ -16,6 +15,13 @@ 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);
@@ -37,7 +43,7 @@ export default class AccessHistoryModal extends React.Component {
}
onShow() {
AsyncClient.getAudits();
this.props.actions.getUserAudits(UserStore.getCurrentId(), 0, 200);
if (!Utils.isMobile()) {
$('.modal-body').perfectScrollbar();
}
@@ -100,7 +106,3 @@ export default class AccessHistoryModal extends React.Component {
);
}
}
AccessHistoryModal.propTypes = {
onHide: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -1,11 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoadingScreen from './loading_screen.jsx';
import LoadingScreen from 'components/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';
@@ -13,9 +12,15 @@ 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 {
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);
@@ -35,7 +40,6 @@ export default class ActivityLogModal extends React.Component {
getStateFromStores() {
return {
sessions: UserStore.getSessions(),
serverError: null,
clientError: null
};
}
@@ -47,18 +51,11 @@ export default class ActivityLogModal extends React.Component {
setTimeout(() => {
modalContent.removeClass('animation--highlight');
}, 1500);
revokeSession(altId,
null,
(err) => {
const state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}
);
this.props.actions.revokeSession(UserStore.getCurrentId(), altId);
}
onShow() {
AsyncClient.getSessions();
this.props.actions.getSessions(UserStore.getCurrentId());
if (!Utils.isMobile()) {
$('.modal-body').perfectScrollbar();
}
@@ -302,7 +299,3 @@ export default class ActivityLogModal extends React.Component {
);
}
}
ActivityLogModal.propTypes = {
onHide: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -10,7 +10,6 @@ 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';
@@ -20,10 +19,20 @@ 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);
@@ -34,11 +43,12 @@ export default class AddUsersToTeam extends React.Component {
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: null,
users: Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)),
values: [],
show: true,
search: false
@@ -50,7 +60,7 @@ export default class AddUsersToTeam extends React.Component {
UserStore.addNotInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
AsyncClient.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
this.props.actions.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
}
componentWillUnmount() {
@@ -97,13 +107,14 @@ export default class AddUsersToTeam extends React.Component {
this.setState({values});
}
onChange(force) {
if (this.state.search && !force) {
return;
onChange() {
let users;
if (this.term) {
users = Object.assign([], searchProfilesNotInCurrentTeam(store.getState(), this.term, true));
} else {
users = Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true));
}
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;
@@ -118,53 +129,25 @@ export default class AddUsersToTeam extends React.Component {
handlePageChange(page, prevPage) {
if (page > prevPage) {
AsyncClient.getProfilesNotInTeam((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
this.props.actions.getProfilesNotInTeam(TeamStore.getCurrentId(), page + 1, USERS_PER_PAGE);
}
}
search(term) {
clearTimeout(this.searchTimeoutId);
this.term = term;
if (term === '') {
this.onChange(true);
this.setState({search: false});
this.searchTimeoutId = '';
this.onChange();
return;
}
const teamId = TeamStore.getCurrentId();
const searchTimeoutId = setTimeout(
this.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});
}
);
searchUsersNotInTeam(term, TeamStore.getCurrentId(), {});
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
handleDelete(values) {
@@ -272,7 +255,3 @@ export default class AddUsersToTeam extends React.Component {
);
}
}
AddUsersToTeam.propTypes = {
onModalDismissed: React.PropTypes.func
};

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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
});
}
);

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ChannelInviteButton from './channel_invite_button.jsx';
import ChannelInviteButton from 'components/channel_invite_button.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import LoadingScreen from './loading_screen.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -19,9 +19,20 @@ 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);
@@ -39,10 +50,9 @@ export default class ChannelInviteModal extends React.Component {
const teamStats = TeamStore.getCurrentStats();
this.state = {
users: null,
users: UserStore.getProfileListNotInChannel(props.channel.id, true),
total: teamStats.active_member_count - channelStats.member_count,
show: true,
search: false,
statusChange: false
};
}
@@ -53,7 +63,7 @@ export default class ChannelInviteModal extends React.Component {
UserStore.addNotInChannelChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onStatusChange);
AsyncClient.getProfilesNotInChannel(this.props.channel.id, 0);
this.props.actions.getProfilesNotInChannel(TeamStore.getCurrentId(), this.props.channel.id, 0);
AsyncClient.getTeamStats(TeamStore.getCurrentId());
}
@@ -64,17 +74,19 @@ export default class ChannelInviteModal extends React.Component {
UserStore.removeStatusesChangeListener(this.onStatusChange);
}
onChange(force) {
if (this.state.search && !force) {
this.search(this.term);
return;
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: UserStore.getProfileListNotInChannel(this.props.channel.id, true),
users,
total: teamStats.active_member_count - channelStats.member_count
});
}
@@ -103,40 +115,24 @@ export default class ChannelInviteModal extends React.Component {
}
nextPage(page) {
AsyncClient.getProfilesNotInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_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(true);
this.setState({search: false});
this.searchTimeoutId = '';
this.onChange();
return;
}
const searchTimeoutId = setTimeout(
this.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});
}
);
searchUsers(term, TeamStore.getCurrentId(), {not_in_channel_id: this.props.channel.id});
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
render() {
@@ -190,8 +186,3 @@ export default class ChannelInviteModal extends React.Component {
);
}
}
ChannelInviteModal.propTypes = {
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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);
}
);

Просмотреть файл

@@ -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);
}
);

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -10,7 +10,6 @@ import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_
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';
@@ -20,10 +19,22 @@ 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);
@@ -36,6 +47,7 @@ export default class MoreDirectChannels extends React.Component {
this.addValue = this.addValue.bind(this);
this.searchTimeoutId = 0;
this.term = '';
this.listType = global.window.mm_config.RestrictDirectMessage;
const values = [];
@@ -63,9 +75,9 @@ export default class MoreDirectChannels extends React.Component {
UserStore.addStatusesChangeListener(this.onChange);
if (this.listType === 'any') {
AsyncClient.getProfiles(0, USERS_PER_PAGE * 2);
this.props.actions.getProfiles(0, USERS_PER_PAGE * 2);
} else {
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
this.props.actions.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
}
}
@@ -134,13 +146,15 @@ export default class MoreDirectChannels extends React.Component {
this.setState({values});
}
onChange(force) {
if (this.state.search && !force) {
return;
}
onChange() {
let users;
if (this.listType === 'any') {
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));
@@ -160,17 +174,20 @@ export default class MoreDirectChannels extends React.Component {
handlePageChange(page, prevPage) {
if (page > prevPage) {
AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
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(true);
this.setState({search: false});
this.searchTimeoutId = '';
this.onChange();
return;
}
@@ -181,37 +198,12 @@ export default class MoreDirectChannels extends React.Component {
teamId = TeamStore.getCurrentId();
}
const searchTimeoutId = setTimeout(
this.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});
}
);
searchUsers(term, teamId);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
handleDelete(values) {
@@ -334,8 +326,3 @@ export default class MoreDirectChannels extends React.Component {
);
}
}
MoreDirectChannels.propTypes = {
startingUsers: React.PropTypes.arrayOf(React.PropTypes.object),
onModalDismissed: React.PropTypes.func
};

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -7,13 +7,12 @@ 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 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 * 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';
@@ -26,6 +25,15 @@ 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);
@@ -239,7 +247,7 @@ export default class PopoverListMembers extends React.Component {
ref='member_popover_target'
onClick={(e) => {
this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover});
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
this.props.actions.getProfilesInChannel(this.props.channel.id, 0);
}}
>
{countText}
@@ -272,8 +280,3 @@ export default class PopoverListMembers extends React.Component {
}
}
PopoverListMembers.propTypes = {
channel: React.PropTypes.object.isRequired,
members: React.PropTypes.array.isRequired,
memberCount: React.PropTypes.number
};

Просмотреть файл

@@ -136,6 +136,7 @@ export default class Root extends React.Component {
);
}
}
Root.defaultProps = {
};

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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();

Просмотреть файл

@@ -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');
}

Просмотреть файл

@@ -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 = [];

Просмотреть файл

@@ -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({

Просмотреть файл

@@ -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;
}

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ConfirmModal from './confirm_modal.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -18,6 +18,14 @@ 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);
@@ -48,7 +56,7 @@ export default class TeamMembersDropdown extends React.Component {
this.props.user.id,
'team_user',
() => {
AsyncClient.getUser(this.props.user.id);
this.props.actions.getUser(this.props.user.id);
if (this.props.user.id === me.id) {
loadMyTeamMembers();
@@ -110,7 +118,7 @@ export default class TeamMembersDropdown extends React.Component {
this.props.user.id,
'team_user team_admin',
() => {
AsyncClient.getUser(this.props.user.id);
this.props.actions.getUser(this.props.user.id);
},
(err) => {
this.setState({serverError: err.message});
@@ -145,7 +153,7 @@ export default class TeamMembersDropdown extends React.Component {
this.props.user.id,
this.state.newRole,
() => {
AsyncClient.getUser(this.props.user.id);
this.props.actions.getUser(this.props.user.id);
const teamUrl = TeamStore.getCurrentTeamUrl();
if (teamUrl) {
@@ -385,8 +393,3 @@ export default class TeamMembersDropdown extends React.Component {
);
}
}
TeamMembersDropdown.propTypes = {
user: React.PropTypes.object.isRequired,
teamMember: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -2,16 +2,15 @@
// 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 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 AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
@@ -80,6 +79,19 @@ const holders = defineMessages({
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;
@@ -205,7 +217,7 @@ class UserSettingsGeneralTab extends React.Component {
updateUser(user, type,
() => {
this.updateSection('');
AsyncClient.getMe();
this.props.actions.getMe();
const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated;
if (verificationEnabled) {
@@ -1194,14 +1206,4 @@ class UserSettingsGeneralTab extends React.Component {
}
}
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);

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -1,15 +1,14 @@
// 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 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 AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -23,6 +22,19 @@ 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);
@@ -98,7 +110,7 @@ export default class SecurityTab extends React.Component {
newPassword,
() => {
this.props.updateSection('');
AsyncClient.getMe();
this.props.actions.getMe();
this.setState(this.getDefaultState());
},
(err) => {
@@ -1022,12 +1034,3 @@ 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
};