mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-55307] Make AdminConsole/TeamFilterDropdown use AsyncSelect instead of LegacyInfiniteScroll (#25294)
This commit is contained in:
@@ -11,10 +11,10 @@
|
||||
}
|
||||
|
||||
.Filter_content.Filter__show {
|
||||
width: 256px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.FilterList.FilterList__full {
|
||||
.FilterList {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user