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
Этот коммит содержится в:
коммит произвёл
Corey Hulen
родитель
9a9729f22f
Коммит
689cac535e
245
webapp/components/searchable_user_list/searchable_user_list.jsx
Обычный файл
245
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user