Commit 57549b71 authored by Catalin Tomai's avatar Catalin Tomai
Browse files

Merge branch 'release-5.26' of https://github.com/mattermost/mattermost-webapp into release-5.26

parents 16a01961 7a585d9d
......@@ -14,8 +14,12 @@
font-size: 12px;
padding: 0 10px;
&.Filter__active, &:hover, &:focus {
background: linear-gradient(0deg, rgba(var(--sys-button-bg-rgb), 0.12), rgba(var(--sys-button-bg-rgb), 0.12)), var(--sys-center-channel-bg);
&.Filter__active, &:focus {
background: rgba(var(--sys-button-bg-rgb), 0.12);
}
&:hover {
background: rgba(var(--sys-button-bg-rgb), .06);
}
}
......@@ -28,12 +32,11 @@
display: none;
position: absolute;
padding: 16px;
padding-top: 8px;
min-width: 256px;
margin-top: 4px;
margin-bottom: 24px;
background: var(--sys-center-channel-bg);
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
box-sizing: border-box;
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12);
......@@ -45,7 +48,8 @@
}
hr {
margin: 8px -16px;
color: rgba(var(--sys-center-channel-color-rgb), 0.08);
margin: 8px -16px 8px -16px;
padding: 0;
}
}
......@@ -76,7 +80,7 @@
.Filter_apply {
float: right;
margin: 0 8px 8px 0px;
margin: 8px 8px 8px 0px;
border-radius: 4px;
font-size: 12px;
......@@ -89,29 +93,34 @@
.FilterList {
display: inline-block;
vertical-align: top;
width: 140px;
padding: 16px 16px 16px 8px;
width: 160px;
padding: 8px;
font-size: 14px;
.FilterList__full {
width: 280px;
&.FilterList__full {
width: 320px;
display: block;
}
.FilterList_name {
font-size: 12px;
font-weight: normal;
padding-bottom: 8px;
font-weight: 600;
padding-bottom: 12px;
color: rgba(var(--sys-center-channel-color-rgb), 0.5);
}
.FilterList_checkbox {
color: var(--sys-center-channel-color);
color: rgba(var(--sys-center-channel-color-rgb), .8);
input {
vertical-align: bottom;
position: relative;
top: -4px;
margin-right: 8px
margin: 0 8px 0 2px;
opacity: 0.64;
&:checked {
opacity: 1;
}
}
label {
......
......@@ -195,7 +195,7 @@ class Filter extends React.PureComponent<Props, State> {
<i className='Icon icon-filter-variant'/>
<FormattedMessage
id='filter.filters'
id='admin.filter.filters'
defaultMessage='Filters'
/>
{filterCount > 0 && ` (${filterCount})`}
......@@ -207,7 +207,7 @@ class Filter extends React.PureComponent<Props, State> {
<div className='Filter_header'>
<div className='Filter_title'>
<FormattedMessage
id='filter.title'
id='admin.filter.title'
defaultMessage='Filter by'
/>
</div>
......@@ -217,7 +217,7 @@ class Filter extends React.PureComponent<Props, State> {
onClick={this.resetFilters}
>
<FormattedMessage
id='filter.reset'
id='admin.filter.reset'
defaultMessage='Reset filters'
/>
</a>
......@@ -235,7 +235,7 @@ class Filter extends React.PureComponent<Props, State> {
onClick={this.onFilter}
>
<FormattedMessage
id='filter.apply'
id='admin.filter.apply'
defaultMessage='Apply'
/>
</button>
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
import {createSelector} from 'reselect';
import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
import {getTeams as fetchTeams, searchTeams} from 'mattermost-redux/actions/teams';
import {getTeams} from 'mattermost-redux/selectors/entities/teams';
import {GlobalState} from 'types/store';
import TeamFilterDropdown from './team_filter_dropdown';
const getSortedListOfTeams = createSelector(
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, page?: number, perPage?: number) => Promise<{ data: any }>;
};
function mapStateToProps(state: GlobalState) {
return {
teams: getSortedListOfTeams(state),
total: state.entities.teams.totalCount || 0,
};
}
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);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
type Props = {
id: string;
name: string;
checked: boolean;
label: string;
updateOption: (checked: boolean, name: string) => void;
};
class TeamFilterCheckbox extends React.PureComponent<Props> {
toggleOption = () => {
const {checked, id, updateOption} = this.props;
updateOption(!checked, id);
}
render() {
const {id, checked, label} = this.props;
return (
<div className='TeamFilterDropdown_checkbox'>
<label>
<input
type='checkbox'
id={id}
name={name}
checked={checked}
onChange={this.toggleOption}
/>
{label}
</label>
</div>
);
}
}
export default TeamFilterCheckbox;
.TeamFilterDropdown {
.TeamFilterDropdownButton {
background: var(--sys-center-channel-bg);
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.16);
box-sizing: border-box;
border-radius: 4px;
width: 100%;
height: 32px;
display: flex;
justify-content: space-between;
font-style: normal;
font-weight: normal;
font-size: 12px;
line-height: 28px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
padding: 1px 0px 1px 4px;
&:hover {
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.48);
}
.TeamFilterDropdownButton_text {
text-align: left;
padding-left: 8px;
flex-grow: 3;
max-width: 230px;
overflow-x: hidden;
text-overflow: ellipsis;
}
.TeamFilterDropdownButton_more {
color: rgba(var(--sys-center-channel-color-rgb), 0.6);
margin-right: 8px;
}
.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-left: 6px;
margin-right: 0;
}
}
.TeamFilterDropdownButton_clear {
display: none;
margin-top: -2px;
padding: 9.5px 0 0 0;
min-width: 36px;
height: 32px;
font-size: 14px;
color: rgba(var(--sys-center-channel-color-rgb), 0.56);
}
&:hover {
.TeamFilterDropdownButton_more {
margin-right: 0;
}
.TeamFilterDropdownButton_clear {
display: block;
}
}
}
.TeamFilterDropdownOptions {
position: absolute;
padding: 16px 16px 0 16px;
margin-top: 2px;
background: var(--sys-center-channel-bg);
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
box-sizing: border-box;
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12);
border-radius: 4px;
z-index: 1;
min-width: 304px;
max-height: 260px;
overflow: hidden;
text-overflow: ellipsis;
display: none;
max-width: 304px;
.TeamFilterDropdownOptions_list {
overflow-y: scroll;
overflow-x: hidden;
margin: 0 -12px 8px -16px;
padding-bottom: 8px;
max-height: 184px;
&::-webkit-scrollbar-track {
background: rgba(var(--sys-center-channel-bg-rgb), 0);
}
}
}
.TeamFilterDropdownOptions__active {
display: block;
}
.TeamFilterDropdown_checkbox {
color: rgba(var(--sys-center-channel-color-rgb), .8);
height: 32px;
margin-right: 4px;
input {
vertical-align: bottom;
position: relative;
top: -4px;
margin-right: 8px;
margin-left: 20px;
padding-top: 6px;
&:hover {
cursor: pointer;
}
opacity: 0.64;
&:checked {
opacity: 1;
}
}
label {
font-weight: normal;
width: 100%;
padding-top: 6px;
height: 32px;
text-overflow: ellipsis;
display: inline-block;
overflow: hidden;
&:hover {
cursor: pointer;
}
}
&:hover {
background: rgba(var(--sys-center-channel-color-rgb), 0.08);
cursor: pointer;
}
}
.TeamFilterDropdown_search {
height: 32px;
width: 100%;
color: var(--sys-center-channel-color);
font-weight: 400;
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.2);
box-sizing: border-box;
border-radius: 4px;
padding: 8px 12px;
font-size: 12px;
line-height: 16px;
&:focus {
margin-left: -1px;
border: 2px solid var(--sys-button-bg);
}
}
.TeamFilterDropdown_reset {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--sys-link-color);
padding: 4px 0px 8px 1px;
}
.TeamFilterDropdown_allTeams {
font-weight: normal;
font-size: 12px;
padding: 4px 0px 8px 1px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
}
.TeamFilterDropdown_divider {
border-top: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.12);
margin: 4px -16px;
}
.TeamFilterDropdown_empty, .TeamFilterDropdown_loading {
font-weight: normal;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
padding-top: 8px;
margin-left: 16px;
margin-bottom: 0;
}
}
// 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 {createSelector} from 'reselect';
import {Team} from 'mattermost-redux/types/teams';
import {debounce} from 'mattermost-redux/actions/helpers';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import InfiniteScroll from 'components/gif_picker/components/InfiniteScroll';
import * as Utils from 'utils/utils.jsx';
import {FilterOption, FilterValues} from '../filter';
import TeamFilterCheckbox from './team_filter_checkbox';
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, page?: number, perPage?: number) => Promise<{ data: any }>;
};
};
type State = {
page: number;
loading: boolean;
show: boolean;
savedSelectedTeams: Team[];
searchResults: Team[];
searchTerm: string;
searchTotal: number;
}
const getSelectedTeams = createSelector(
(selectedTeamIds: string[]) => selectedTeamIds,
(selectedTeamIds: string[], teams: Team[]) => teams,
(selectedTeamIds, teams) => teams.filter((team) => selectedTeamIds.includes(team.id)),
);
const getFilteredTeams = createSelector(
(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)); // eslint-disable-line camelcase, @typescript-eslint/camelcase
},
);
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);
this.state = {
page: 0,
loading: false,
show: false,
savedSelectedTeams: [],
searchResults: [],
searchTerm: '',
searchTotal: 0,
};
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;
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
this.props.actions.getData(0, TEAMS_PER_PAGE);
}
componentWillUnmount = () => {
document.removeEventListener('mousedown', this.handleClickOutside);
}
hidePopover = () => {
this.setState({show: false});
}
togglePopover = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (this.state.show) {
this.hidePopover();
return;
}
if (this.clearRef?.current?.contains(event.target as Node)) {
return;
}
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;
}
this.hidePopover();
}
setScrollPosition = (event: React.UIEvent<HTMLDivElement, UIEvent>) => {
this.scrollPosition = (event.target as HTMLDivElement).scrollTop;
}
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.props.teams.length;
}
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;