From 1af078299106b312785fb508368c73e289e4c64d Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Thu, 28 Dec 2023 10:09:35 +0000 Subject: [PATCH] [MM-55307] Make AdminConsole/TeamFilterDropdown use AsyncSelect instead of LegacyInfiniteScroll (#25294) --- .../channel_list/channel_list.scss | 4 +- .../admin_console/filter/filter.scss | 51 +- .../filter/team_filter_dropdown/index.tsx | 42 +- .../team_filter_checkbox.tsx | 42 -- .../team_filter_dropdown.scss | 195 ------- .../team_filter_dropdown.tsx | 475 ++++-------------- .../components/InfiniteScroll/index.js | 149 ------ webapp/channels/src/i18n/en.json | 8 +- 8 files changed, 162 insertions(+), 804 deletions(-) delete mode 100644 webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_checkbox.tsx delete mode 100644 webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.scss delete mode 100644 webapp/channels/src/components/gif_picker/components/InfiniteScroll/index.js diff --git a/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/channel_list.scss b/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/channel_list.scss index e8ad96be5a..35874a46bc 100644 --- a/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/channel_list.scss +++ b/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/channel_list.scss @@ -11,10 +11,10 @@ } .Filter_content.Filter__show { - width: 256px; + width: 320px; } - .FilterList.FilterList__full { + .FilterList { width: auto; } } diff --git a/webapp/channels/src/components/admin_console/filter/filter.scss b/webapp/channels/src/components/admin_console/filter/filter.scss index 989e01b1f3..16b3837efc 100644 --- a/webapp/channels/src/components/admin_console/filter/filter.scss +++ b/webapp/channels/src/components/admin_console/filter/filter.scss @@ -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); diff --git a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/index.tsx b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/index.tsx index 000f65ea51..5961711505 100644 --- a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/index.tsx +++ b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/index.tsx @@ -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; -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators, Actions>({ - getData: (page, pageSize) => fetchTeams(page, pageSize, true), - searchTeams, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(TeamFilterDropdown); +export default connector(TeamFilterDropdown); diff --git a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_checkbox.tsx b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_checkbox.tsx deleted file mode 100644 index 6dfce26a5a..0000000000 --- a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_checkbox.tsx +++ /dev/null @@ -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 ( -
- -
- ); -}; - -export default React.memo(TeamFilterCheckbox); diff --git a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.scss b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.scss deleted file mode 100644 index 1496dd80fd..0000000000 --- a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.scss +++ /dev/null @@ -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; - } -} diff --git a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx index c096eaf718..277821299d 100644 --- a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx +++ b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx @@ -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 { - private ref: React.RefObject; - private searchRef: React.RefObject; - private clearRef: React.RefObject; - private listRef: React.RefObject; - 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>([]); + 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) => { - if (this.state.show) { - this.hidePopover(); + function handleOnChange(value: ValueType, actionMeta: ActionMeta) { + 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) => { - 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) => { - 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: ( - - ), - 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 ( - - ); - }; - - 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 ? () : (); - - return ( -
-
- {this.props.option.name} -
- -
- - -
- - - {selectedTeamIds.length > 0 && ( - - - - )} - - {selectedTeamIds.length === 0 && ( -
- -
- )} - -
- {selectedTeams} - - {selectedTeams.length > 0 &&
} - - - {teamsToDisplay} - - - {this.state.loading && ( -
- - -
- )} - - {teamsToDisplay.length === 0 && selectedTeams.length === 0 && !this.state.loading && ( -
- -
- )} -
-
-
+ return ( +
+
+ {props.option.name}
- ); - } + 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: () => , + }} + /> +
+ ); } export default TeamFilterDropdown; diff --git a/webapp/channels/src/components/gif_picker/components/InfiniteScroll/index.js b/webapp/channels/src/components/gif_picker/components/InfiniteScroll/index.js deleted file mode 100644 index 3b61e7b8c6..0000000000 --- a/webapp/channels/src/components/gif_picker/components/InfiniteScroll/index.js +++ /dev/null @@ -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; - } -} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 5b8fabe84d..a88467a324 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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 Settings > Display > Language (leave this field blank to have all supported languages available). If you're manually adding new languages, the Default Client Language must be added before saving this setting.\n \nWould like to help with translations? Join the Mattermost Translation Server to contribute.", "admin.general.localization.availableLocalesNoResults": "No results found",