summaryrefslogtreecommitdiffstats
path: root/webapp/components/searchable_user_list
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2017-03-30 12:46:47 -0400
committerCorey Hulen <corey@hulen.com>2017-03-30 09:46:47 -0700
commit689cac535e45c47a4f603b236dc129dd456efcc9 (patch)
tree767ef80b310d6d073840bd5216da38c439f6e193 /webapp/components/searchable_user_list
parent9a9729f22fea7275637eafb4046900c9f372ec56 (diff)
downloadchat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.gz
chat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.bz2
chat-689cac535e45c47a4f603b236dc129dd456efcc9.zip
PLT-2713/PLT-6028 Added System Users list to System Console (#5882)
* PLT-2713 Added ability for admins to list users not in any team * Updated style of unit test * Split SearchableUserList to give better control over its properties * Added users without any teams to the user store * Added ManageUsers page * Renamed ManageUsers to SystemUsers * Added ability to search by user id in SystemUsers page * Added SystemUsersDropdown * Removed unnecessary injectIntl * Created TeamUtils * Reduced scope of system console heading CSS * Added team filter to TeamAnalytics page * Updated admin console sidebar * Removed unnecessary TODO * Removed unused reference to deleted modal * Fixed system console sidebar not scrolling on first load * Fixed TeamAnalytics page not rendering on first load * Fixed chart.js throwing an error when switching between teams * Changed TeamAnalytics header to show the team's display name * Fixed appearance of TeamAnalytics and SystemUsers on small screen widths * Fixed placement of 'No users found' message * Fixed teams not appearing in SystemUsers on first load * Updated user count text for SystemUsers * Changed search by id fallback to trigger less often * Fixed SystemUsers list items not updating when searching * Fixed localization strings for SystemUsers page
Diffstat (limited to 'webapp/components/searchable_user_list')
-rw-r--r--webapp/components/searchable_user_list/searchable_user_list.jsx245
-rw-r--r--webapp/components/searchable_user_list/searchable_user_list_container.jsx72
2 files changed, 317 insertions, 0 deletions
diff --git a/webapp/components/searchable_user_list/searchable_user_list.jsx b/webapp/components/searchable_user_list/searchable_user_list.jsx
new file mode 100644
index 000000000..91e0205b0
--- /dev/null
+++ b/webapp/components/searchable_user_list/searchable_user_list.jsx
@@ -0,0 +1,245 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import UserList from 'components/user_list.jsx';
+
+import * as Utils from 'utils/utils.jsx';
+
+const NEXT_BUTTON_TIMEOUT = 500;
+
+export default class SearchableUserList extends React.Component {
+ static propTypes = {
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ usersPerPage: React.PropTypes.number,
+ total: React.PropTypes.number,
+ extraInfo: React.PropTypes.object,
+ nextPage: React.PropTypes.func.isRequired,
+ previousPage: React.PropTypes.func.isRequired,
+ search: React.PropTypes.func.isRequired,
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object,
+ actionUserProps: React.PropTypes.object,
+ focusOnMount: React.PropTypes.bool,
+ renderCount: React.PropTypes.func,
+ renderFilterRow: React.PropTypes.func,
+
+ page: React.PropTypes.number.isRequired,
+ term: React.PropTypes.string.isRequired,
+ onTermChange: React.PropTypes.func.isRequired
+ };
+
+ static defaultProps = {
+ users: [],
+ usersPerPage: 50, // eslint-disable-line no-magic-numbers
+ extraInfo: {},
+ actions: [],
+ actionProps: {},
+ actionUserProps: {},
+ showTeamToggle: false,
+ focusOnMount: false
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.nextPage = this.nextPage.bind(this);
+ this.previousPage = this.previousPage.bind(this);
+ this.focusSearchBar = this.focusSearchBar.bind(this);
+
+ this.handleInput = this.handleInput.bind(this);
+
+ this.renderCount = this.renderCount.bind(this);
+
+ this.nextTimeoutId = 0;
+
+ this.state = {
+ nextDisabled: false
+ };
+ }
+
+ componentDidMount() {
+ this.focusSearchBar();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.page !== prevProps.page || this.props.term !== prevProps.term) {
+ this.refs.userList.scrollToTop();
+ }
+
+ this.focusSearchBar();
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.nextTimeoutId);
+ }
+
+ nextPage(e) {
+ e.preventDefault();
+
+ this.setState({nextDisabled: true});
+ this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT);
+
+ this.props.nextPage();
+ }
+
+ previousPage(e) {
+ e.preventDefault();
+
+ this.props.previousPage();
+ }
+
+ focusSearchBar() {
+ if (this.props.focusOnMount) {
+ this.refs.filter.focus();
+ }
+ }
+
+ handleInput(e) {
+ this.props.onTermChange(e.target.value);
+ this.props.search(e.target.value);
+ }
+
+ renderCount(users) {
+ if (!users) {
+ return null;
+ }
+
+ const count = users.length;
+ const total = this.props.total;
+ const isSearch = Boolean(this.props.term);
+
+ let startCount;
+ let endCount;
+ if (isSearch) {
+ startCount = -1;
+ endCount = -1;
+ } else {
+ startCount = this.props.page * this.props.usersPerPage;
+ endCount = startCount + count;
+ }
+
+ if (this.props.renderCount) {
+ return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch);
+ }
+
+ if (this.props.total) {
+ if (isSearch) {
+ return (
+ <FormattedMessage
+ id='filtered_user_list.countTotal'
+ defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
+ values={{
+ count,
+ total
+ }}
+ />
+ );
+ }
+
+ return (
+ <FormattedMessage
+ id='filtered_user_list.countTotalPage'
+ defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
+ values={{
+ count,
+ startCount: startCount + 1,
+ endCount,
+ total
+ }}
+ />
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ let nextButton;
+ let previousButton;
+ let usersToDisplay;
+
+ if (this.props.term || !this.props.users) {
+ usersToDisplay = this.props.users;
+ } else if (!this.props.term) {
+ const pageStart = this.props.page * this.props.usersPerPage;
+ const pageEnd = pageStart + this.props.usersPerPage;
+ usersToDisplay = this.props.users.slice(pageStart, pageEnd);
+
+ if (usersToDisplay.length >= this.props.usersPerPage) {
+ nextButton = (
+ <button
+ className='btn btn-default filter-control filter-control__next'
+ onClick={this.nextPage}
+ disabled={this.state.nextDisabled}
+ >
+ <FormattedMessage
+ id='filtered_user_list.next'
+ defaultMessage='Next'
+ />
+ </button>
+ );
+ }
+
+ if (this.props.page > 0) {
+ previousButton = (
+ <button
+ className='btn btn-default filter-control filter-control__prev'
+ onClick={this.previousPage}
+ >
+ <FormattedMessage
+ id='filtered_user_list.prev'
+ defaultMessage='Previous'
+ />
+ </button>
+ );
+ }
+ }
+
+ let filterRow;
+ if (this.props.renderFilterRow) {
+ filterRow = this.props.renderFilterRow(this.handleInput);
+ } else {
+ filterRow = (
+ <div className='col-xs-12'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
+ value={this.props.term}
+ onInput={this.handleInput}
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div className='filtered-user-list'>
+ <div className='filter-row'>
+ {filterRow}
+ <div className='col-sm-12'>
+ <span className='member-count pull-left'>{this.renderCount(usersToDisplay)}</span>
+ </div>
+ </div>
+ <div
+ className='more-modal__list'
+ >
+ <UserList
+ ref='userList'
+ users={usersToDisplay}
+ extraInfo={this.props.extraInfo}
+ actions={this.props.actions}
+ actionProps={this.props.actionProps}
+ actionUserProps={this.props.actionUserProps}
+ />
+ </div>
+ <div className='filter-controls'>
+ {previousButton}
+ {nextButton}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/searchable_user_list/searchable_user_list_container.jsx b/webapp/components/searchable_user_list/searchable_user_list_container.jsx
new file mode 100644
index 000000000..816dec062
--- /dev/null
+++ b/webapp/components/searchable_user_list/searchable_user_list_container.jsx
@@ -0,0 +1,72 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import SearchableUserList from './searchable_user_list.jsx';
+
+export default class SearchableUserListContainer extends React.Component {
+ static propTypes = {
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ usersPerPage: React.PropTypes.number,
+ total: React.PropTypes.number,
+ extraInfo: React.PropTypes.object,
+ nextPage: React.PropTypes.func.isRequired,
+ search: React.PropTypes.func.isRequired,
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object,
+ actionUserProps: React.PropTypes.object,
+ focusOnMount: React.PropTypes.bool
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.handleTermChange = this.handleTermChange.bind(this);
+
+ this.nextPage = this.nextPage.bind(this);
+ this.previousPage = this.previousPage.bind(this);
+ this.search = this.search.bind(this);
+
+ this.state = {
+ term: '',
+ page: 0
+ };
+ }
+
+ handleTermChange(term) {
+ this.setState({term});
+ }
+
+ nextPage() {
+ this.setState({page: this.state.page + 1});
+
+ this.props.nextPage(this.state.page + 1);
+ }
+
+ previousPage() {
+ this.setState({page: this.state.page - 1});
+ }
+
+ search(term) {
+ this.props.search(term);
+
+ if (term !== '') {
+ this.setState({page: 0});
+ }
+ }
+
+ render() {
+ return (
+ <SearchableUserList
+ {...this.props}
+ nextPage={this.nextPage}
+ previousPage={this.previousPage}
+ search={this.search}
+ page={this.state.page}
+ term={this.state.term}
+ onTermChange={this.handleTermChange}
+ />
+ );
+ }
+}