From 689cac535e45c47a4f603b236dc129dd456efcc9 Mon Sep 17 00:00:00 2001
From: Harrison Healey <harrisonmhealey@gmail.com>
Date: Thu, 30 Mar 2017 12:46:47 -0400
Subject: 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
---
 .../searchable_user_list/searchable_user_list.jsx  | 245 +++++++++++++++++++++
 1 file changed, 245 insertions(+)
 create mode 100644 webapp/components/searchable_user_list/searchable_user_list.jsx

(limited to 'webapp/components/searchable_user_list/searchable_user_list.jsx')

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>
+        );
+    }
+}
-- 
cgit v1.2.3-1-g7c22

