[MM-55307] Make AdminConsole/TeamFilterDropdown use AsyncSelect instead of LegacyInfiniteScroll (#25294)

This commit is contained in:
M-ZubairAhmed
2023-12-28 10:09:35 +00:00
committed by GitHub
parent 81a1d725a0
commit 1af0782991
8 changed files with 162 additions and 804 deletions

View File

@@ -11,10 +11,10 @@
}
.Filter_content.Filter__show {
width: 256px;
width: 320px;
}
.FilterList.FilterList__full {
.FilterList {
width: auto;
}
}

View File

@@ -90,7 +90,7 @@
&.FilterList__full {
display: block;
width: 320px;
width: 100%;
}
.FilterList_name {
@@ -100,6 +100,55 @@
font-weight: 600;
}
.filterListSelect {
&__control {
min-height: 32px;
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.16);
background: var(--sys-center-channel-bg);
border-radius: 4px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
font-size: 12px;
&:hover {
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.48);
}
}
&__option {
background: var(--sys-center-channel-bg);
color: rgba(var(--sys-center-channel-color-rgb), 0.8);
cursor: pointer;
font-size: 12px;
&:hover {
background: rgba(var(--sys-center-channel-color-rgb), 0.08);
}
}
&__indicator {
padding: 9px;
color: rgba(var(--sys-center-channel-color-rgb), 0.56);
cursor: pointer;
> svg {
width: 14px;
height: 14px;
}
}
&__indicator-separator {
background-color: rgba(var(--sys-center-channel-color-rgb), 0.16);
}
&__multi-value__remove {
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.16);
color: inherit;
cursor: pointer;
}
}
}
.FilterList_checkbox {
color: rgba(var(--sys-center-channel-color-rgb), 0.8);

View File

@@ -1,46 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch, ActionCreatorsMapObject} from 'redux';
import {connect, type ConnectedProps} from 'react-redux';
import type {TeamSearchOpts} from '@mattermost/types/teams';
import {getTeams as fetchTeams, searchTeams} from 'mattermost-redux/actions/teams';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getTeams} from 'mattermost-redux/selectors/entities/teams';
import type {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
import type {GlobalState} from 'types/store';
import {getTeams, searchTeams} from 'mattermost-redux/actions/teams';
import TeamFilterDropdown from './team_filter_dropdown';
const getSortedListOfTeams = createSelector(
'getSortedListOfTeams',
const mapDispatchToProps = {
searchTeams,
getTeams,
(teams) => Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name)),
);
type Actions = {
getData: (page: number, perPage: number) => Promise<{ data: any }>;
searchTeams: (term: string, opts: TeamSearchOpts) => Promise<{ data: any }>;
};
function mapStateToProps(state: GlobalState) {
return {
teams: getSortedListOfTeams(state),
total: state.entities.teams.totalCount || 0,
};
}
const connector = connect(null, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
getData: (page, pageSize) => fetchTeams(page, pageSize, true),
searchTeams,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TeamFilterDropdown);
export default connector(TeamFilterDropdown);

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
type Props = {
id: string;
name: string;
checked: boolean;
label: string;
updateOption: (checked: boolean, name: string) => void;
};
const TeamFilterCheckbox = ({
id,
name,
checked,
label,
updateOption,
}: Props) => {
const toggleOption = useCallback(() => {
updateOption(!checked, id);
}, [checked, id, updateOption]);
return (
<div className='TeamFilterDropdown_checkbox'>
<label>
<input
type='checkbox'
id={id}
name={name}
checked={checked}
onChange={toggleOption}
/>
{label}
</label>
</div>
);
};
export default React.memo(TeamFilterCheckbox);

View File

@@ -1,195 +0,0 @@
.TeamFilterDropdown {
.TeamFilterDropdownButton {
display: flex;
width: 100%;
height: 32px;
box-sizing: border-box;
justify-content: space-between;
padding: 1px 0 1px 4px;
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.16);
background: var(--sys-center-channel-bg);
border-radius: 4px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
font-size: 12px;
font-style: normal;
font-weight: normal;
line-height: 28px;
&:hover {
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.48);
.TeamFilterDropdownButton_more {
margin-right: 0;
}
.TeamFilterDropdownButton_clear {
display: block;
}
}
.TeamFilterDropdownButton_text {
max-width: 230px;
flex-grow: 3;
padding-left: 8px;
overflow-x: hidden;
text-align: left;
text-overflow: ellipsis;
}
.TeamFilterDropdownButton_more {
margin-right: 8px;
color: rgba(var(--sys-center-channel-color-rgb), 0.6);
}
.TeamFilterDropdownButton_icon {
min-width: 32px;
height: 31px;
border-left: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.16);
margin-top: -1px;
.Icon {
display: flex;
width: 14px;
height: 14px;
padding-top: 8px;
margin-right: 0;
margin-left: 6px;
}
}
.TeamFilterDropdownButton_clear {
display: none;
min-width: 36px;
height: 32px;
padding: 9.5px 0 0 0;
margin-top: -2px;
color: rgba(var(--sys-center-channel-color-rgb), 0.56);
font-size: 14px;
}
}
.TeamFilterDropdownOptions {
position: absolute;
z-index: 1;
display: none;
overflow: hidden;
min-width: 304px;
max-width: 304px;
max-height: 260px;
box-sizing: border-box;
padding: 16px 16px 0 16px;
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
margin-top: 2px;
background: var(--sys-center-channel-bg);
border-radius: 4px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
text-overflow: ellipsis;
.TeamFilterDropdownOptions_list {
max-height: 184px;
padding-bottom: 8px;
margin: 0 -12px 8px -16px;
overflow-x: hidden;
overflow-y: scroll;
&::-webkit-scrollbar-track {
background: rgba(var(--sys-center-channel-bg-rgb), 0);
}
}
}
.TeamFilterDropdownOptions__active {
display: block;
}
.TeamFilterDropdown_checkbox {
height: 32px;
margin-right: 4px;
color: rgba(var(--sys-center-channel-color-rgb), 0.8);
input {
position: relative;
top: -4px;
padding-top: 6px;
margin-right: 8px;
margin-left: 20px;
opacity: 0.64;
vertical-align: bottom;
&:hover {
cursor: pointer;
}
&:checked {
opacity: 1;
}
}
label {
display: inline-block;
overflow: hidden;
width: 100%;
height: 32px;
padding-top: 6px;
font-weight: normal;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
&:hover {
background: rgba(var(--sys-center-channel-color-rgb), 0.08);
cursor: pointer;
}
}
.TeamFilterDropdown_search {
width: 100%;
height: 32px;
box-sizing: border-box;
padding: 8px 12px;
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.2);
border-radius: 4px;
color: var(--sys-center-channel-color);
font-size: 12px;
font-weight: 400;
line-height: 16px;
&:focus {
border: 2px solid var(--sys-button-bg);
margin-left: -1px;
}
}
.TeamFilterDropdown_reset {
display: block;
padding: 4px 0 8px 1px;
color: var(--sys-link-color);
font-size: 12px;
font-weight: 600;
}
.TeamFilterDropdown_allTeams {
padding: 4px 0 8px 1px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
font-size: 12px;
font-weight: normal;
}
.TeamFilterDropdown_divider {
border-top: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.12);
margin: 4px -16px;
}
.TeamFilterDropdown_empty,
.TeamFilterDropdown_loading {
padding-top: 8px;
margin-bottom: 0;
margin-left: 16px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
font-weight: normal;
}
}

View File

@@ -1,408 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import React, {useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import type {ActionMeta, OptionsType, ValueType} from 'react-select';
import AsyncSelect from 'react-select/async';
import type {Team, TeamSearchOpts} from '@mattermost/types/teams';
import type {Team} from '@mattermost/types/teams';
import {debounce} from 'mattermost-redux/actions/helpers';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import type {ActionResult} from 'mattermost-redux/types/actions';
import InfiniteScroll from 'components/gif_picker/components/InfiniteScroll';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import * as Utils from 'utils/utils';
import TeamFilterCheckbox from './team_filter_checkbox';
import type {FilterOption, FilterValues} from '../filter';
import './team_filter_dropdown.scss';
import '../filter.scss';
type Props = {
option: FilterOption;
optionKey: string;
updateValues: (values: FilterValues, optionKey: string) => void;
teams: Team[];
total: number;
actions: {
getData: (page: number, perPage: number) => Promise<{ data: any }>;
searchTeams: (term: string, opts: TeamSearchOpts) => Promise<{ data: any }>;
};
};
type State = {
page: number;
loading: boolean;
show: boolean;
savedSelectedTeams: Team[];
searchResults: Team[];
searchTerm: string;
searchTotal: number;
}
const getSelectedTeams = createSelector(
'getSelectedTeams',
(selectedTeamIds: string[]) => selectedTeamIds,
(selectedTeamIds: string[], teams: Team[]) => teams,
(selectedTeamIds, teams) => teams.filter((team) => selectedTeamIds.includes(team.id)),
);
const getFilteredTeams = createSelector(
'getFilteredTeams',
(term: string) => term.trim().toLowerCase(),
(term: string, teams: Team[]) => teams,
(term: string, teams: Team[]) => {
return teams.filter((team: Team) => team?.display_name?.toLowerCase().includes(term));
},
);
import type {PropsFromRedux} from './index';
const TEAMS_PER_PAGE = 50;
const MAX_BUTTON_TEXT_LENGTH = 30;
const INITIAL_SEARCH_RETRY_TIMEOUT = 300;
class TeamFilterDropdown extends React.PureComponent<Props, State> {
private ref: React.RefObject<HTMLDivElement>;
private searchRef: React.RefObject<HTMLInputElement>;
private clearRef: React.RefObject<HTMLInputElement>;
private listRef: React.RefObject<HTMLDivElement>;
private searchRetryInterval: number;
private searchRetryId: number;
private scrollPosition: number;
public constructor(props: Props) {
super(props);
type TeamSelectOption = {label: string; value: string}
this.state = {
page: 0,
loading: false,
show: false,
savedSelectedTeams: [],
searchResults: [],
searchTerm: '',
searchTotal: 0,
};
export interface Props extends PropsFromRedux {
option: FilterOption;
updateValues: (values: FilterValues, optionKey: string) => void;
}
this.ref = React.createRef();
this.searchRef = React.createRef();
this.clearRef = React.createRef();
this.listRef = React.createRef();
this.searchRetryInterval = INITIAL_SEARCH_RETRY_TIMEOUT;
this.searchRetryId = 0;
this.scrollPosition = 0;
function TeamFilterDropdown(props: Props) {
const {formatMessage} = useIntl();
const [list, setList] = useState<OptionsType<TeamSelectOption>>([]);
const [pageNumber, setPageNumber] = useState(0);
async function loadListInPageNumber(page: number) {
try {
const response = await props.getTeams(page, TEAMS_PER_PAGE, true) as ActionResult<{teams: Team[]}>;
if (response && response.data && response.data.teams && response.data.teams.length > 0) {
const list = response.data.teams.
map((team: Team) => ({
value: team.id,
label: team.display_name,
})).
sort((a: TeamSelectOption, b: TeamSelectOption) => a.label.localeCompare(b.label));
if (page === 0) {
setList(list);
} else {
setList((existingList) => [...existingList, ...list]);
}
setPageNumber(page + 1);
}
} catch (error) {
console.error(error); // eslint-disable-line no-console
}
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
this.props.actions.getData(0, TEAMS_PER_PAGE);
async function searchInList(term: string, callBack: (options: OptionsType<{label: string; value: string}>) => void) {
try {
const response = await props.searchTeams(term, {page: 0, per_page: TEAMS_PER_PAGE}) as ActionResult<{teams: Team[]}>;
if (response && response.data && response.data.teams && response.data.teams.length > 0) {
const teams = response.data.teams.map((team: Team) => ({
value: team.id,
label: team.display_name,
}));
callBack(teams);
}
callBack([]);
} catch (error) {
console.error(error); // eslint-disable-line no-console
callBack([]);
}
}
componentWillUnmount = () => {
document.removeEventListener('mousedown', this.handleClickOutside);
};
function handleMenuScrolledToBottom() {
loadListInPageNumber(pageNumber);
}
hidePopover = () => {
this.setState({show: false});
};
togglePopover = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (this.state.show) {
this.hidePopover();
function handleOnChange(value: ValueType<TeamSelectOption>, actionMeta: ActionMeta<TeamSelectOption>) {
if (!actionMeta.action) {
return;
}
if (this.clearRef?.current?.contains(event.target as Node)) {
return;
let selected = [];
if (Array.isArray(value) && value.length > 0) {
selected = value.map((v) => v.value);
}
const selectedTeamIds = this.props.option.values.team_ids.value as string[];
const selectedTeams = getSelectedTeams(selectedTeamIds, this.props.teams);
const savedSelectedTeams = selectedTeams.sort((a, b) => a.display_name.localeCompare(b.display_name));
this.setState({show: true, savedSelectedTeams, searchTerm: ''}, () => {
this.searchRef?.current?.focus();
if (this.listRef?.current) {
this.listRef.current.scrollTop = 0;
}
});
};
handleClickOutside = (event: MouseEvent) => {
if (this.ref?.current?.contains(event.target as Node)) {
return;
if (actionMeta.action === 'clear') {
props.updateValues({team_ids: {name: 'Teams', value: []}}, 'teams');
} else if (actionMeta.action === 'select-option' || actionMeta.action === 'remove-value') {
props.updateValues({team_ids: {name: 'Teams', value: selected}}, 'teams');
}
this.hidePopover();
};
}
setScrollPosition = (event: React.UIEvent<HTMLDivElement, UIEvent>) => {
this.scrollPosition = (event.target as HTMLDivElement).scrollTop;
};
useEffect(() => {
loadListInPageNumber(0);
}, []);
hasMore = (): boolean => {
if (this.state.loading) {
return false;
} else if (this.state.searchTerm.length > 0) {
return this.state.searchTotal > this.state.searchResults.length;
}
return this.props.total > (this.state.page + 1) * TEAMS_PER_PAGE;
};
const optionValues = props.option.values?.team_ids?.value as string[];
const selectedValues = list.filter((item) => optionValues.includes(item.value));
loadMore = async () => {
const {searchTerm, loading} = this.state;
if (loading) {
return;
}
this.setState({loading: true});
const page = this.state.page + 1;
if (searchTerm.length > 0) {
this.searchTeams(searchTerm, page);
} else {
await this.props.actions.getData(page, TEAMS_PER_PAGE);
}
if (this.listRef?.current) {
this.listRef.current.scrollTop = this.scrollPosition;
}
this.setState({page, loading: false});
};
searchTeams = async (term: string, page: number) => {
let searchResults = [];
let searchTotal = 0;
const response = await this.props.actions.searchTeams(term, {page, per_page: TEAMS_PER_PAGE});
if (response?.data) {
const {data} = response;
searchResults = page > 0 ? this.state.searchResults.concat(data.teams) : data.teams;
searchTotal = data.total_count;
this.setState({page, loading: false, searchResults, searchTotal, searchTerm: term});
return;
}
this.searchRetryInterval *= 2;
this.searchRetryId = window.setTimeout(this.searchTeams.bind(null, term, page), this.searchRetryInterval);
};
searchTeamsDebounced = debounce((page, term) => this.searchTeams(term, page), INITIAL_SEARCH_RETRY_TIMEOUT, false, () => {});
handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value;
if (this.searchRetryId !== 0) {
clearTimeout(this.searchRetryId);
this.searchRetryId = 0;
this.searchRetryInterval = INITIAL_SEARCH_RETRY_TIMEOUT;
}
if (searchTerm.length === 0) {
const selectedTeamIds = this.props.option.values.team_ids.value as string[];
const selectedTeams = getSelectedTeams(selectedTeamIds, this.props.teams);
const savedSelectedTeams = selectedTeams.sort((a, b) => a.display_name.localeCompare(b.display_name));
this.setState({searchTerm, savedSelectedTeams, searchResults: [], searchTotal: 0, page: 0});
} else {
this.setState({loading: true, searchTerm, searchResults: [], searchTotal: 0, page: 0});
}
this.searchTeamsDebounced(0, searchTerm);
};
resetTeams = () => {
this.setState({savedSelectedTeams: [], show: false, searchResults: [], searchTotal: 0, page: 0, searchTerm: ''});
this.props.updateValues({team_ids: {name: 'Teams', value: []}}, 'teams');
};
toggleTeam = (checked: boolean, teamId: string) => {
const prevSelectedTeamIds = this.props.option.values.team_ids.value as string[];
let selectedTeamIds;
if (checked) {
selectedTeamIds = [...prevSelectedTeamIds, teamId];
} else {
selectedTeamIds = prevSelectedTeamIds.filter((id) => id !== teamId);
}
this.props.updateValues({team_ids: {name: 'Teams', value: selectedTeamIds}}, 'teams');
};
generateButtonText = () => {
const selectedTeamIds = this.props.option.values.team_ids.value as string[];
if (selectedTeamIds.length === 0) {
return {
buttonText: (
<FormattedMessage
id='admin.filter.all_teams'
defaultMessage='All Teams'
/>
),
buttonMore: 0,
};
}
const selectedTeams = getSelectedTeams(selectedTeamIds, this.props.teams);
let buttonText = '';
let buttonMore = 0;
let buttonOverflowed = false;
selectedTeams.forEach((team, index) => {
buttonOverflowed = buttonOverflowed || !(MAX_BUTTON_TEXT_LENGTH > (buttonText.length + team.display_name.length));
if (index === 0) {
buttonText += team.display_name;
} else if (buttonOverflowed) {
buttonMore += 1;
} else {
buttonText = `${buttonText}, ${team.display_name}`;
}
});
return {buttonText, buttonMore};
};
render() {
const selectedTeamIds = this.props.option.values.team_ids.value as string[];
const {buttonText, buttonMore} = this.generateButtonText();
const createFilterCheckbox = (team: Team) => {
return (
<TeamFilterCheckbox
id={team.id}
name={team.id}
checked={selectedTeamIds.includes(team.id)}
updateOption={this.toggleTeam}
label={team.display_name}
key={team.id}
/>
);
};
let visibleTeams = this.state.searchResults;
let selectedTeams: JSX.Element[] = [];
if (this.state.searchTerm.length === 0) {
visibleTeams = this.props.teams.slice(0, (this.state.page + 1) * TEAMS_PER_PAGE).filter((team) => !this.state.savedSelectedTeams.some((selectedTeam) => selectedTeam.id === team.id));
selectedTeams = this.state.savedSelectedTeams.map(createFilterCheckbox);
} else {
visibleTeams = getFilteredTeams(this.state.searchTerm, visibleTeams);
}
const teamsToDisplay = visibleTeams.map(createFilterCheckbox);
const chevron = this.state.show ? (<i className='Icon icon-chevron-up'/>) : (<i className='Icon icon-chevron-down'/>);
return (
<div
className='FilterList FilterList__full'
>
<div className='FilterList_name'>
{this.props.option.name}
</div>
<div
className='TeamFilterDropdown'
ref={this.ref}
>
<button
type='button'
className='TeamFilterDropdownButton'
onClick={this.togglePopover}
>
<div className='TeamFilterDropdownButton_text'>
{buttonText}
</div>
{buttonMore > 0 && (
<div className='TeamFilterDropdownButton_more'>
<FormattedMessage
id='admin.filter.count_more'
defaultMessage='+{count, number} more'
values={{count: buttonMore}}
/>
</div>
)}
{selectedTeamIds.length > 0 && (
<i
className={'TeamFilterDropdownButton_clear fa fa-times-circle'}
onClick={this.resetTeams}
ref={this.clearRef}
/>
)}
<div className='TeamFilterDropdownButton_icon'>
{chevron}
</div>
</button>
<div className={this.state.show ? 'TeamFilterDropdownOptions TeamFilterDropdownOptions__active' : 'TeamFilterDropdownOptions'}>
<input
className='TeamFilterDropdown_search'
type='text'
placeholder={Utils.localizeMessage('search_bar.search', 'Search')}
value={this.state.searchTerm}
onChange={this.handleSearch}
ref={this.searchRef}
/>
{selectedTeamIds.length > 0 && (
<a
className='TeamFilterDropdown_reset'
onClick={this.resetTeams}
>
<FormattedMessage
id='admin.filter.reset_teams'
defaultMessage='Reset to all teams'
/>
</a>
)}
{selectedTeamIds.length === 0 && (
<div
className='TeamFilterDropdown_allTeams'
>
<FormattedMessage
id='admin.filter.showing_all_teams'
defaultMessage='Showing all teams'
/>
</div>
)}
<div
className='TeamFilterDropdownOptions_list'
ref={this.listRef}
onScroll={this.setScrollPosition}
>
{selectedTeams}
{selectedTeams.length > 0 && <div className='TeamFilterDropdown_divider'/>}
<InfiniteScroll
hasMore={this.hasMore()}
loadMore={this.loadMore}
threshold={50}
useWindow={false}
initialLoad={false}
>
{teamsToDisplay}
</InfiniteScroll>
{this.state.loading && (
<div className='TeamFilterDropdown_loading'>
<LoadingSpinner/>
<FormattedMessage
id='admin.data_grid.loading'
defaultMessage='Loading'
/>
</div>
)}
{teamsToDisplay.length === 0 && selectedTeams.length === 0 && !this.state.loading && (
<div className='TeamFilterDropdown_empty'>
<FormattedMessage
id='admin.filter.no_results'
defaultMessage='No items match'
/>
</div>
)}
</div>
</div>
</div>
return (
<div className='FilterList FilterList__full'>
<div className='FilterList_name'>
{props.option.name}
</div>
);
}
<AsyncSelect
inputId='adminConsoleTeamFilterDropdown'
isMulti={true}
isClearable={true}
hideSelectedOptions={true}
classNamePrefix='filterListSelect'
cacheOptions={false}
placeholder={formatMessage({id: 'admin.channels.filterBy.team.placeholder', defaultMessage: 'Search and select teams'})}
loadingMessage={() => formatMessage({id: 'admin.channels.filterBy.team.loading', defaultMessage: 'Loading teams'})}
noOptionsMessage={() => formatMessage({id: 'admin.channels.filterBy.team.noTeams', defaultMessage: 'No teams found'})}
loadOptions={searchInList}
defaultOptions={list}
value={selectedValues}
onChange={handleOnChange}
onMenuScrollToBottom={handleMenuScrolledToBottom}
components={{
LoadingIndicator: () => <LoadingSpinner/>,
}}
/>
</div>
);
}
export default TeamFilterDropdown;

View File

@@ -1,149 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
export default class InfiniteScroll extends PureComponent {
static propTypes = {
children: PropTypes.array,
element: PropTypes.string,
hasMore: PropTypes.bool,
initialLoad: PropTypes.bool,
loader: PropTypes.object,
loadMore: PropTypes.func.isRequired,
pageStart: PropTypes.number,
threshold: PropTypes.number,
useWindow: PropTypes.bool,
isReverse: PropTypes.bool,
containerHeight: PropTypes.number,
scrollPosition: PropTypes.number,
};
static defaultProps = {
element: 'div',
hasMore: false,
initialLoad: true,
pageStart: 0,
threshold: 250,
useWindow: true,
isReverse: false,
containerHeight: null,
scrollPosition: null,
};
componentDidMount() {
this.pageLoaded = this.props.pageStart;
this.attachScrollListener();
this.setScrollPosition();
}
componentDidUpdate() {
this.attachScrollListener();
}
render() {
const {
children,
element,
hasMore,
initialLoad, // eslint-disable-line no-unused-vars
loader,
loadMore, // eslint-disable-line no-unused-vars
pageStart, // eslint-disable-line no-unused-vars
threshold, // eslint-disable-line no-unused-vars
useWindow, // eslint-disable-line no-unused-vars
isReverse, // eslint-disable-line no-unused-vars
scrollPosition, // eslint-disable-line no-unused-vars
containerHeight,
...props
} = this.props;
props.ref = (node) => {
this.scrollComponent = node;
};
const elementProps = containerHeight ? {...props, style: {height: containerHeight}} : props;
return React.createElement(element, elementProps, children, hasMore && (loader || this.defaultLoader));
}
calculateTopPosition(el) {
if (!el) {
return 0;
}
return el.offsetTop + this.calculateTopPosition(el.offsetParent);
}
setScrollPosition() {
const {scrollPosition} = this.props;
if (scrollPosition !== null) {
window.scrollTo(0, scrollPosition);
}
}
scrollListener = () => {
const el = this.scrollComponent;
const scrollEl = window;
let offset;
if (this.props.useWindow) {
var scrollTop = ('pageYOffset' in scrollEl) ? scrollEl.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
if (this.props.isReverse) {
offset = scrollTop;
} else {
offset = this.calculateTopPosition(el) + (el.offsetHeight - scrollTop - window.innerHeight);
}
} else if (this.props.isReverse) {
offset = el.parentNode.scrollTop;
} else {
offset = el.scrollHeight - el.parentNode.scrollTop - el.parentNode.clientHeight;
}
if (offset < Number(this.props.threshold)) {
this.detachScrollListener();
// Call loadMore after detachScrollListener to allow for non-async loadMore functions
if (typeof this.props.loadMore === 'function') {
this.props.loadMore(this.pageLoaded += 1);
}
}
};
attachScrollListener() {
if (!this.props.hasMore) {
return;
}
let scrollEl = window;
if (this.props.useWindow === false) {
scrollEl = this.scrollComponent.parentNode;
}
scrollEl.addEventListener('scroll', this.scrollListener);
scrollEl.addEventListener('resize', this.scrollListener);
if (this.props.initialLoad) {
this.scrollListener();
}
}
detachScrollListener() {
var scrollEl = window;
if (this.props.useWindow === false) {
scrollEl = this.scrollComponent.parentNode;
}
scrollEl.removeEventListener('scroll', this.scrollListener);
scrollEl.removeEventListener('resize', this.scrollListener);
}
componentWillUnmount() {
this.detachScrollListener();
}
// Set a defaut loader for all your `InfiniteScroll` components
setDefaultLoader(loader) {
this.defaultLoader = loader;
}
}

View File

@@ -527,6 +527,9 @@
"admin.channel_settings.description": "Manage channel settings.",
"admin.channel_settings.groupsPageTitle": "{siteName} Channels",
"admin.channel_settings.title": "Channels",
"admin.channels.filterBy.team.loading": "Loading teams",
"admin.channels.filterBy.team.noTeams": "No teams found",
"admin.channels.filterBy.team.placeholder": "Search and select teams",
"admin.cluster.ClusterName": "Cluster Name:",
"admin.cluster.ClusterNameDesc": "The cluster to join by name. Only nodes with the same cluster name will join together. This is to support Blue-Green deployments or staging pointing to the same database.",
"admin.cluster.ClusterNameEx": "E.g.: \"Production\" or \"Staging\"",
@@ -1024,14 +1027,9 @@
"admin.file.enableMobileDownloadTitle": "Allow File Downloads on Mobile:",
"admin.file.enableMobileUploadDesc": "When false, disables file uploads on mobile apps. If Allow File Sharing is set to true, users can still upload files from a mobile web browser.",
"admin.file.enableMobileUploadTitle": "Allow File Uploads on Mobile:",
"admin.filter.all_teams": "All Teams",
"admin.filter.apply": "Apply",
"admin.filter.count_more": "+{count, number} more",
"admin.filter.filters": "Filters",
"admin.filter.no_results": "No items match",
"admin.filter.reset": "Reset filters",
"admin.filter.reset_teams": "Reset to all teams",
"admin.filter.showing_all_teams": "Showing all teams",
"admin.filter.title": "Filter by",
"admin.general.localization.availableLocalesDescription": "Set which languages are available for users in <strong>Settings > Display > Language</strong> (leave this field blank to have all supported languages available). If you're manually adding new languages, the <strong>Default Client Language</strong> must be added before saving this setting.\n \nWould like to help with translations? Join the <link>Mattermost Translation Server</link> to contribute.",
"admin.general.localization.availableLocalesNoResults": "No results found",