diff --git a/actions/views/search.js b/actions/views/search.js index fb5abf022d49..1bd336b0ed14 100644 --- a/actions/views/search.js +++ b/actions/views/search.js @@ -13,6 +13,26 @@ export function setModalSearchTerm(term) { }; } +export function setUserGridSearch(term) { + return async (dispatch) => { + dispatch({ + type: SearchTypes.SET_USER_GRID_SEARCH, + data: term, + }); + return {data: true}; + }; +} + +export function setUserGridFilters(filters = {}) { + return async (dispatch) => { + dispatch({ + type: SearchTypes.SET_USER_GRID_FILTERS, + data: filters, + }); + return {data: true}; + }; +} + export function setSystemUsersSearch(term, team = '', filter = '') { return async (dispatch) => { dispatch({ diff --git a/components/add_users_to_team_modal/index.ts b/components/add_users_to_team_modal/index.ts index c360aa0fda36..d4df7aa63e3b 100644 --- a/components/add_users_to_team_modal/index.ts +++ b/components/add_users_to_team_modal/index.ts @@ -28,7 +28,7 @@ type Actions = { function mapStateToProps(state: GlobalState, props: Props) { const {id: teamId} = props.team; - let filterOptions: {} = {skipInactive: true}; + let filterOptions: {} = {active: true}; if (props.filterExcludeGuests) { filterOptions = {role: 'system_user', ...filterOptions}; } diff --git a/components/admin_console/data_grid/data_grid.scss b/components/admin_console/data_grid/data_grid.scss index 586b590fb011..0bf35d8c99fc 100644 --- a/components/admin_console/data_grid/data_grid.scss +++ b/components/admin_console/data_grid/data_grid.scss @@ -88,7 +88,7 @@ .DataGrid_row { border-left: 2px solid transparent; - + &:nth-child(odd) { background-color: $bg--gray; } diff --git a/components/admin_console/data_grid/data_grid.tsx b/components/admin_console/data_grid/data_grid.tsx index 49fff64a9742..cc7045241625 100644 --- a/components/admin_console/data_grid/data_grid.tsx +++ b/components/admin_console/data_grid/data_grid.tsx @@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl'; import NextIcon from 'components/widgets/icons/fa_next_icon'; import PreviousIcon from 'components/widgets/icons/fa_previous_icon'; import LoadingSpinner from 'components/widgets/loading/loading_spinner'; +import {FilterOptions} from 'components/admin_console/filter/filter'; import DataGridHeader from './data_grid_header'; import DataGridRow from './data_grid_row'; @@ -54,6 +55,12 @@ type Props = { search: (term: string) => void; term: string; searchPlaceholder?: string; + + filterProps?: { + options: FilterOptions; + keys: string[]; + onFilter: (options: FilterOptions) => void; + }; }; type State = { @@ -182,6 +189,7 @@ class DataGrid extends React.PureComponent { onSearch={this.search} placeholder={this.props.searchPlaceholder || ''} term={this.props.term} + filterProps={this.props.filterProps} /> ); } diff --git a/components/admin_console/data_grid/data_grid_search.tsx b/components/admin_console/data_grid/data_grid_search.tsx index a8c25c755f25..1726d8ac8931 100644 --- a/components/admin_console/data_grid/data_grid_search.tsx +++ b/components/admin_console/data_grid/data_grid_search.tsx @@ -7,29 +7,62 @@ import FaSearchIcon from 'components/widgets/icons/fa_search_icon'; import * as Utils from 'utils/utils.jsx'; +import Filter, {FilterOptions} from 'components/admin_console/filter/filter'; + import './data_grid.scss'; type Props = { onSearch: (term: string) => void; placeholder: string; term: string; + + filterProps?: { + options: FilterOptions; + keys: string[]; + onFilter: (options: FilterOptions) => void; + }; +} + +type State = { + term: string; } -class DataGridSearch extends React.PureComponent { +class DataGridSearch extends React.PureComponent { + public constructor(props: Props) { + super(props); + + this.state = { + term: '', + }; + } + handleSearch = (e: React.ChangeEvent) => { const term = e.target.value; + this.setState({term}); this.props.onSearch(term); } resetSearch = () => { this.props.onSearch(''); - } + }; + + onFilter = (filters: FilterOptions) => { + this.props.filterProps?.onFilter(filters); + }; render() { + const {filterProps} = this.props; + let {placeholder} = this.props; if (!placeholder) { placeholder = Utils.localizeMessage('search_bar.search', 'Search'); } + + let filter; + if (filterProps) { + filter = ; + } + return (
@@ -52,6 +85,8 @@ class DataGridSearch extends React.PureComponent { data-testid='clear-search' />
+ + {filter}
); } diff --git a/components/admin_console/filter/filter.scss b/components/admin_console/filter/filter.scss new file mode 100644 index 000000000000..99fa300a3fc1 --- /dev/null +++ b/components/admin_console/filter/filter.scss @@ -0,0 +1,122 @@ +.Filter { + padding-left: 8px; + position: relative; + display: inline-block; + + .Filter_button { + width: auto; + height: 32px; + + background: var(--sys-center-channel-bg); + border-radius: 4px; + border: 0px; + color: var(--sys-button-bg); + 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); + } + } + + .Icon { + margin-right: 2px; + font-size: 14px; + } + + .Filter_content { + display: none; + position: absolute; + padding: 16px; + padding-top: 8px; + min-width: 256px; + 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); + border-radius: 4px; + z-index: 1; + + &.Filter__show { + display: block; + } + + hr { + margin: 8px -16px; + padding: 0; + } + } + + .Filter_header { + display: flex; + flex-direction: row; + justify-content: space-between; + + .Filter_title { + font-weight: bold; + font-size: 14px; + line-height: 14px; + padding: 8px; + color: var(--sys-center-channel-color); + } + + .Filter_reset { + font-size: 12px; + line-height: 16px; + padding: 8px + } + } + + .Filter_lists { + white-space: nowrap; + } + + .Filter_apply { + float: right; + margin: 0 8px 8px 0px; + border-radius: 4px; + font-size: 12px; + + &:disabled { + background: rgba(var(--sys-center-channel-color-rgb), 0.1); + color: var(--sys-center-channel-color); + } + } + + .FilterList { + display: inline-block; + vertical-align: top; + width: 140px; + padding: 16px 16px 16px 8px; + font-size: 14px; + + .FilterList__full { + width: 280px; + display: block; + } + + .FilterList_name { + font-size: 12px; + font-weight: normal; + padding-bottom: 8px; + } + + .FilterList_checkbox { + color: var(--sys-center-channel-color); + + input { + vertical-align: bottom; + position: relative; + top: -4px; + margin-right: 8px + } + + label { + font-weight: normal; + } + } + } +} diff --git a/components/admin_console/filter/filter.tsx b/components/admin_console/filter/filter.tsx new file mode 100644 index 000000000000..f8964be6a027 --- /dev/null +++ b/components/admin_console/filter/filter.tsx @@ -0,0 +1,248 @@ +// 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 classNames from 'classnames'; + +import FilterList from './filter_list'; +import './filter.scss'; + +export type Filters = { + [filterKey: string]: string[]; +}; + +export type FilterValue = { + name: string | JSX.Element; + value: boolean | string | string[]; +}; + +export type FilterValues = { + [key: string]: FilterValue; +}; + +export type FilterOption = { + + // Display name of the filter option eg. 'Channels', 'Roles' or + name: string | JSX.Element; + + // List of keys that match the filter values, used to define the order in which the filters appear + keys: string[]; + + // Key value map of filter values with keys matching the keys above + values: FilterValues; + + // Filter Component type, optional parameter defaults to FilterCheckbox + type?: React.ElementType; +} + +export type FilterOptions = { + [key: string]: FilterOption; +} + +type Props = { + onFilter: (filters: FilterOptions) => void; + options: FilterOptions; + keys: string[]; +} + +type State = { + show: boolean; + options: FilterOptions; + keys: string[]; + optionsModified: boolean; + filterCount: number; +} + +class Filter extends React.PureComponent { + private buttonRef: React.RefObject; + private filterRef: React.RefObject; + + public constructor(props: Props) { + super(props); + + let options = {...props.options}; + let keys = [...props.keys]; + let valid = true; + keys.forEach((key) => { + const option = options[key]; + if (option && valid) { + option.keys.forEach((optionKey) => { + if (!option.values[optionKey]) { + valid = false; + } + }); + } else { + valid = false; + } + }); + + if (!valid) { + options = {}; + keys = []; + } + + this.state = { + show: false, + options, + keys, + optionsModified: false, + filterCount: 0, + }; + + this.filterRef = React.createRef(); + this.buttonRef = React.createRef(); + } + + componentDidMount = () => { + document.addEventListener('mousedown', this.handleClickOutside); + } + + componentWillUnmount = () => { + document.removeEventListener('mousedown', this.handleClickOutside); + } + + handleClickOutside = (event: MouseEvent) => { + if (this.filterRef?.current?.contains(event.target as Node)) { + return; + } + this.hidePopover(); + } + + hidePopover = () => { + this.setState({show: false}); + this.buttonRef?.current?.blur(); + } + + togglePopover = () => { + if (this.state.show) { + this.hidePopover(); + return; + } + + this.setState({show: true}); + } + + updateValues = async (values: FilterValues, optionKey: string) => { + const options = { + ...this.state.options, + [optionKey]: { + ...this.state.options[optionKey], + values: { + ...values + }, + }, + }; + this.setState({options, optionsModified: true}); + } + + onFilter = () => { + this.props.onFilter(this.state.options); + this.setState({optionsModified: false, show: false, filterCount: this.calculateFilterCount()}); + } + + calculateFilterCount = () => { + const options = this.state.options; + let filterCount = 0; + this.props.keys.forEach((key) => { + const {values, keys} = options[key]; + keys.forEach((filterKey: string) => { + if (values[filterKey].value instanceof Array) { + filterCount += (values[filterKey].value as string[]).length; + } else if (values[filterKey].value) { + filterCount += 1; + } + }); + }); + return filterCount; + } + + resetFilters = () => { + this.setState({options: {...this.props.options}}, this.onFilter); + } + + renderFilterOptions = () => { + const {keys, options} = this.state; + return keys.map((key: string) => { + const filter = options[key]; + const FilterListComponent = filter.type || FilterList; + + return ( + + ); + }); + } + + render() { + const filters = this.renderFilterOptions(); + const {filterCount} = this.state; + + return ( +
+ + +
+
+
+ +
+ + + + +
+ +
+ +
+ {filters} +
+ + +
+
+ ); + } +} + +export default Filter; diff --git a/components/admin_console/filter/filter_checkbox.tsx b/components/admin_console/filter/filter_checkbox.tsx new file mode 100644 index 000000000000..013a3ac57d74 --- /dev/null +++ b/components/admin_console/filter/filter_checkbox.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +type Props = { + name: string; + checked: boolean; + label: string | JSX.Element; + updateOption: (checked: boolean, name: string) => void; +}; + +class FilterCheckbox extends React.PureComponent { + toggleOption = () => { + const {checked, name, updateOption} = this.props; + updateOption(!checked, name); + } + + render() { + const {name, checked, label} = this.props; + return ( +
+ +
+ ); + } +} + +export default FilterCheckbox; diff --git a/components/admin_console/filter/filter_list.tsx b/components/admin_console/filter/filter_list.tsx new file mode 100644 index 000000000000..51c816706387 --- /dev/null +++ b/components/admin_console/filter/filter_list.tsx @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {FilterOption, FilterValues} from './filter'; +import FilterCheckbox from './filter_checkbox'; +import './filter.scss'; + +type Props = { + option: FilterOption; + optionKey: string; + updateValues: (values: FilterValues, optionKey: string) => void; +} + +class FilterList extends React.PureComponent { + updateOption = async (value: boolean, key: string) => { + const values = {...this.props.option.values}; + values[key].value = value; + await this.props.updateValues(values, this.props.optionKey); + } + + render() { + const {option} = this.props; + const valuesToRender = option.keys.map((optionKey: string, index: number) => { + const currentValue = option.values[optionKey]; + const {value, name} = currentValue; + const FilterItem = option.type || FilterCheckbox; + + return ( +
+ +
+ ); + }); + + return ( +
+
+ {option.name} +
+ + {valuesToRender} +
+ ); + } +} + +export default FilterList; diff --git a/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap b/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap index 859d948e4007..de76240ff972 100644 --- a/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap +++ b/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap @@ -60,6 +60,63 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match > , + "values": Object { + "channel_admin": Object { + "name": , + "value": false, + }, + "channel_user": Object { + "name": , + "value": false, + }, + "system_admin": Object { + "name": , + "value": false, + }, + "system_guest": Object { + "name": , + "value": false, + }, + }, + }, + }, + } + } includeUsers={Object {}} loadPage={[Function]} loading={true} @@ -324,6 +381,63 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match > , + "values": Object { + "channel_admin": Object { + "name": , + "value": false, + }, + "channel_user": Object { + "name": , + "value": false, + }, + "system_admin": Object { + "name": , + "value": false, + }, + "system_guest": Object { + "name": , + "value": false, + }, + }, + }, + }, + } + } includeUsers={Object {}} loadPage={[Function]} loading={true} diff --git a/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.test.tsx b/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.test.tsx index 6aa4719ab16e..594161a6b412 100644 --- a/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.test.tsx +++ b/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.test.tsx @@ -21,6 +21,7 @@ describe('admin_console/team_channel_settings/channel/ChannelMembers', () => { const channel: Channel = Object.assign(TestHelper.getChannelMock({id: 'channel-1'})); const baseProps = { + filters: {}, usersToRemove: {}, usersToAdd: {}, onAddCallback: jest.fn(), @@ -37,11 +38,14 @@ describe('admin_console/team_channel_settings/channel/ChannelMembers', () => { }, totalCount: 3, searchTerm: '', + enableGuestAccounts: true, actions: { getChannelStats: jest.fn(), loadProfilesAndReloadChannelMembers: jest.fn(), searchProfilesAndChannelMembers: jest.fn(), - setSystemUsersSearch: jest.fn(), + getFilteredUsersStats: jest.fn(), + setUserGridSearch: jest.fn(), + setUserGridFilters: jest.fn(), }, }; diff --git a/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.tsx b/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.tsx index 609818916667..7df39b57bc68 100644 --- a/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.tsx +++ b/components/admin_console/team_channel_settings/channel/details/channel_members/channel_members.tsx @@ -6,8 +6,10 @@ import {FormattedMessage} from 'react-intl'; import {Dictionary} from 'mattermost-redux/types/utilities'; -import {UserProfile} from 'mattermost-redux/types/users'; +import {ServerError} from 'mattermost-redux/types/errors'; +import {UserProfile, UsersStats, GetFilteredUsersStatsOpts} from 'mattermost-redux/types/users'; import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; +import GeneralConstants from 'mattermost-redux/constants/general'; import {t} from 'utils/i18n'; import Constants from 'utils/constants'; @@ -17,25 +19,27 @@ import UserGrid from 'components/admin_console/user_grid/user_grid'; import {BaseMembership} from 'components/admin_console/user_grid/user_grid_role_dropdown'; import ChannelInviteModal from 'components/channel_invite_modal'; import ToggleModalButton from 'components/toggle_modal_button'; +import {FilterOptions} from 'components/admin_console/filter/filter'; type Props = { channelId: string; channel: Channel; + filters: GetFilteredUsersStatsOpts; users: UserProfile[]; usersToRemove: Dictionary; usersToAdd: Dictionary; channelMembers: Dictionary; - totalCount: number; + totalCount: number; + searchTerm: string; loading?: boolean; + enableGuestAccounts: boolean; onAddCallback: (users: UserProfile[]) => void; onRemoveCallback: (user: UserProfile) => void; updateRole: (userId: string, schemeUser: boolean, schemeAdmin: boolean) => void; - searchTerm: string; - actions: { getChannelStats: (channelId: string) => Promise<{ data: boolean; @@ -46,7 +50,14 @@ type Props = { searchProfilesAndChannelMembers: (term: string, options?: {}) => Promise<{ data: boolean; }>; - setSystemUsersSearch: (term: string) => Promise<{ + getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ + data?: UsersStats; + error?: ServerError; + }>; + setUserGridSearch: (term: string) => Promise<{ + data: boolean; + }>; + setUserGridFilters: (filters: GetFilteredUsersStatsOpts) => Promise<{ data: boolean; }>; }; @@ -73,29 +84,35 @@ export default class ChannelMembers extends React.PureComponent { public componentDidMount() { const {channelId} = this.props; - const {loadProfilesAndReloadChannelMembers, getChannelStats, setSystemUsersSearch} = this.props.actions; + const {loadProfilesAndReloadChannelMembers, getChannelStats, setUserGridSearch, setUserGridFilters} = this.props.actions; Promise.all([ - setSystemUsersSearch(''), + setUserGridSearch(''), + setUserGridFilters({}), getChannelStats(channelId), - loadProfilesAndReloadChannelMembers(0, PROFILE_CHUNK_SIZE * 2, channelId), + loadProfilesAndReloadChannelMembers(0, PROFILE_CHUNK_SIZE * 2, channelId, '', {active: true}), ]).then(() => this.setStateLoading(false)); } - public componentDidUpdate(prevProps: Props) { - if (prevProps.searchTerm !== this.props.searchTerm) { + public async componentDidUpdate(prevProps: Props) { + const filtersModified = JSON.stringify(prevProps.filters) !== JSON.stringify(this.props.filters); + const searchTermModified = prevProps.searchTerm !== this.props.searchTerm; + if (filtersModified || searchTermModified) { this.setStateLoading(true); clearTimeout(this.searchTimeoutId); - const searchTerm = this.props.searchTerm; + const {searchTerm, filters} = this.props; if (searchTerm === '') { this.searchTimeoutId = 0; + if (filtersModified) { + await prevProps.actions.loadProfilesAndReloadChannelMembers(0, PROFILE_CHUNK_SIZE * 2, prevProps.channelId, '', {active: true, ...filters}); + } this.setStateLoading(false); return; } const searchTimeoutId = window.setTimeout( async () => { - await prevProps.actions.searchProfilesAndChannelMembers(searchTerm, {in_channel_id: this.props.channelId, allow_inactive: false}); + await prevProps.actions.searchProfilesAndChannelMembers(searchTerm, {...filters, in_channel_id: this.props.channelId, allow_inactive: false}); if (searchTimeoutId !== this.searchTimeoutId) { return; @@ -115,8 +132,8 @@ export default class ChannelMembers extends React.PureComponent { private loadPage = async (page: number) => { const {loadProfilesAndReloadChannelMembers} = this.props.actions; - const {channelId} = this.props; - await loadProfilesAndReloadChannelMembers(page + 1, PROFILE_CHUNK_SIZE, channelId); + const {channelId, filters} = this.props; + await loadProfilesAndReloadChannelMembers(page + 1, PROFILE_CHUNK_SIZE, channelId, '', {active: true, ...filters}); } private removeUser = (user: UserProfile) => { @@ -128,15 +145,104 @@ export default class ChannelMembers extends React.PureComponent { } private search = async (term: string) => { - this.props.actions.setSystemUsersSearch(term); + this.props.actions.setUserGridSearch(term); } private updateMembership = (membership: BaseMembership) => { this.props.updateRole(membership.user_id, membership.scheme_user, membership.scheme_admin); } + private onFilter = async (filterOptions: FilterOptions) => { + const roles = filterOptions.role.values; + const systemRoles: string[] = []; + const channelRoles: string[] = []; + let filters = {}; + Object.keys(roles).forEach((filterKey: string) => { + if (roles[filterKey].value) { + if (filterKey.includes('channel')) { + channelRoles.push(filterKey); + } else { + systemRoles.push(filterKey); + } + } + }); + + if (systemRoles.length > 0 || channelRoles.length > 0) { + if (systemRoles.length > 0) { + filters = {roles: systemRoles}; + } + if (channelRoles.length > 0) { + filters = {...filters, channel_roles: channelRoles}; + } + this.props.actions.setUserGridFilters(filters); + this.props.actions.getFilteredUsersStats({in_channel: this.props.channelId, include_bots: true, ...filters}); + } else { + this.props.actions.setUserGridFilters(filters); + } + } + render = () => { const {users, channel, channelId, usersToAdd, usersToRemove, channelMembers, totalCount, searchTerm} = this.props; + const filterOptions: FilterOptions = { + role: { + name: ( + + ), + values: { + [GeneralConstants.SYSTEM_GUEST_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.CHANNEL_USER_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.CHANNEL_ADMIN_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.SYSTEM_ADMIN_ROLE]: { + name: ( + + ), + value: false, + }, + }, + keys: [GeneralConstants.SYSTEM_GUEST_ROLE, GeneralConstants.CHANNEL_USER_ROLE, GeneralConstants.CHANNEL_ADMIN_ROLE, GeneralConstants.SYSTEM_ADMIN_ROLE], + }, + }; + + if (!this.props.enableGuestAccounts) { + delete filterOptions.role.values[GeneralConstants.SYSTEM_GUEST_ROLE]; + filterOptions.role.keys = [GeneralConstants.CHANNEL_USER_ROLE, GeneralConstants.CHANNEL_ADMIN_ROLE, GeneralConstants.SYSTEM_ADMIN_ROLE]; + } + const filterProps = { + options: filterOptions, + keys: ['role'], + onFilter: this.onFilter, + }; + return ( { excludeUsers={usersToRemove} term={searchTerm} scope={'channel'} + filterProps={filterProps} /> ); diff --git a/components/admin_console/team_channel_settings/channel/details/channel_members/index.ts b/components/admin_console/team_channel_settings/channel/details/channel_members/index.ts index 78faa2b62454..9e7ea6b4da9b 100644 --- a/components/admin_console/team_channel_settings/channel/details/channel_members/index.ts +++ b/components/admin_console/team_channel_settings/channel/details/channel_members/index.ts @@ -5,7 +5,8 @@ import {connect} from 'react-redux'; import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux'; import {Dictionary} from 'mattermost-redux/types/utilities'; -import {UserProfile} from 'mattermost-redux/types/users'; +import {ServerError} from 'mattermost-redux/types/errors'; +import {UserProfile, UsersStats, GetFilteredUsersStatsOpts} from 'mattermost-redux/types/users'; import {filterProfilesMatchingTerm, profileListToMap} from 'mattermost-redux/utils/user_utils'; @@ -13,12 +14,14 @@ import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions'; import {ChannelStats} from 'mattermost-redux/types/channels'; import {getChannelStats} from 'mattermost-redux/actions/channels'; +import {getFilteredUsersStats} from 'mattermost-redux/actions/users'; import {getChannelMembersInChannels, getAllChannelStats, getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {searchProfilesInChannel, makeGetProfilesInChannel, filterProfiles} from 'mattermost-redux/selectors/entities/users'; +import {makeGetProfilesInChannel, makeSearchProfilesInChannel, filterProfiles, getFilteredUsersStats as selectFilteredUsersStats} from 'mattermost-redux/selectors/entities/users'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {loadProfilesAndReloadChannelMembers, searchProfilesAndChannelMembers} from 'actions/user_actions'; -import {setSystemUsersSearch} from 'actions/views/search'; +import {setUserGridSearch, setUserGridFilters} from 'actions/views/search'; import {GlobalState} from 'types/store'; import ChannelMembers from './channel_members'; @@ -39,7 +42,14 @@ type Actions = { searchProfilesAndChannelMembers: (term: string, options?: {}) => Promise<{ data: boolean; }>; - setSystemUsersSearch: (term: string) => Promise<{ + getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ + data?: UsersStats; + error?: ServerError; + }>; + setUserGridSearch: (term: string) => Promise<{ + data: boolean; + }>; + setUserGridFilters: (filters: GetFilteredUsersStatsOpts) => Promise<{ data: boolean; }>; }; @@ -53,40 +63,53 @@ function searchUsersToAdd(users: Dictionary, term: string): Diction function makeMapStateToProps() { const doGetProfilesInChannel = makeGetProfilesInChannel(); + const doSearchProfilesInChannel = makeSearchProfilesInChannel(); return function mapStateToProps(state: GlobalState, props: Props) { const {channelId, usersToRemove} = props; let {usersToAdd} = props; + const config = getConfig(state); const channelMembers = getChannelMembersInChannels(state)[channelId] || {}; - const channel = getChannel(state, channelId) || {channel_id: channelId}; + const searchTerm = state.views.search.userGridSearch?.term || ''; + const filters = state.views.search.userGridSearch?.filters || {}; + + let totalCount: number; + if (Object.keys(filters).length === 0) { + const stats: ChannelStats = getAllChannelStats(state)[channelId] || { + member_count: 0, + channel_id: channelId, + pinnedpost_count: 0, + guest_count: 0, + }; + totalCount = stats.member_count; + } else { + const filteredUserStats: UsersStats = selectFilteredUsersStats(state) || { + total_users_count: 0, + }; + totalCount = filteredUserStats.total_users_count; + } - const stats: ChannelStats = getAllChannelStats(state)[channelId] || { - member_count: 0, - channel_id: channelId, - pinnedpost_count: 0, - guest_count: 0, - }; - - const searchTerm = state.views.search.systemUsersSearch?.term || ''; let users = []; if (searchTerm) { - users = searchProfilesInChannel(state, channelId, searchTerm, false, true); + users = doSearchProfilesInChannel(state, channelId, searchTerm, false, {...filters, active: true}); usersToAdd = searchUsersToAdd(usersToAdd, searchTerm); } else { - users = doGetProfilesInChannel(state, channelId, true); + users = doGetProfilesInChannel(state, channelId, {...filters, active: true}); } return { + filters, channelId, channel, users, channelMembers, usersToAdd, usersToRemove, - totalCount: stats.member_count, + totalCount, searchTerm, + enableGuestAccounts: config.EnableGuestAccounts === 'true', }; }; } @@ -97,7 +120,9 @@ function mapDispatchToProps(dispatch: Dispatch) { getChannelStats, loadProfilesAndReloadChannelMembers, searchProfilesAndChannelMembers, - setSystemUsersSearch, + getFilteredUsersStats, + setUserGridSearch, + setUserGridFilters, }, dispatch), }; } diff --git a/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap b/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap index 2c8b7daf079f..5da2ca4b4f73 100644 --- a/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap +++ b/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap @@ -58,6 +58,63 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps > , + "values": Object { + "system_admin": Object { + "name": , + "value": false, + }, + "system_guest": Object { + "name": , + "value": false, + }, + "team_admin": Object { + "name": , + "value": false, + }, + "team_user": Object { + "name": , + "value": false, + }, + }, + }, + }, + } + } includeUsers={Object {}} loadPage={[Function]} loading={true} @@ -296,6 +353,63 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps > , + "values": Object { + "system_admin": Object { + "name": , + "value": false, + }, + "system_guest": Object { + "name": , + "value": false, + }, + "team_admin": Object { + "name": , + "value": false, + }, + "team_user": Object { + "name": , + "value": false, + }, + }, + }, + }, + } + } includeUsers={Object {}} loadPage={[Function]} loading={true} diff --git a/components/admin_console/team_channel_settings/team/details/team_members/index.ts b/components/admin_console/team_channel_settings/team/details/team_members/index.ts index 0933048ca7e6..6cc6606e6e47 100644 --- a/components/admin_console/team_channel_settings/team/details/team_members/index.ts +++ b/components/admin_console/team_channel_settings/team/details/team_members/index.ts @@ -5,19 +5,22 @@ import {connect} from 'react-redux'; import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux'; import {Dictionary} from 'mattermost-redux/types/utilities'; -import {UserProfile} from 'mattermost-redux/types/users'; +import {ServerError} from 'mattermost-redux/types/errors'; +import {UserProfile, UsersStats, GetFilteredUsersStatsOpts} from 'mattermost-redux/types/users'; import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions'; import {filterProfilesMatchingTerm, profileListToMap} from 'mattermost-redux/utils/user_utils'; import {getTeamStats as loadTeamStats} from 'mattermost-redux/actions/teams'; +import {getFilteredUsersStats} from 'mattermost-redux/actions/users'; import {getMembersInTeams, getTeamStats, getTeam} from 'mattermost-redux/selectors/entities/teams'; -import {getProfilesInTeam, searchProfilesInTeam, filterProfiles} from 'mattermost-redux/selectors/entities/users'; +import {getProfilesInTeam, searchProfilesInTeam, filterProfiles, getFilteredUsersStats as selectFilteredUsersStats} from 'mattermost-redux/selectors/entities/users'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {GlobalState} from 'types/store'; import {loadProfilesAndReloadTeamMembers, searchProfilesAndTeamMembers} from 'actions/user_actions.jsx'; -import {setSystemUsersSearch} from 'actions/views/search'; +import {setUserGridSearch, setUserGridFilters} from 'actions/views/search'; import TeamMembers from './team_members'; @@ -37,7 +40,14 @@ type Actions = { searchProfilesAndTeamMembers: (term: string, options?: {}) => Promise<{ data: boolean; }>; - setSystemUsersSearch: (term: string) => Promise<{ + getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ + data?: UsersStats; + error?: ServerError; + }>; + setUserGridSearch: (term: string) => Promise<{ + data: boolean; + }>; + setUserGridFilters: (filters: GetFilteredUsersStatsOpts) => Promise<{ data: boolean; }>; }; @@ -55,26 +65,40 @@ function mapStateToProps(state: GlobalState, props: Props) { const teamMembers = getMembersInTeams(state)[teamId] || {}; const team = getTeam(state, teamId) || {}; - const stats = getTeamStats(state)[teamId] || {active_member_count: 0}; + const config = getConfig(state); + const searchTerm = state.views.search.userGridSearch?.term || ''; + const filters = state.views.search.userGridSearch?.filters || {}; + + let totalCount: number; + if (Object.keys(filters).length === 0) { + const stats = getTeamStats(state)[teamId] || {active_member_count: 0}; + totalCount = stats.active_member_count; + } else { + const filteredUserStats: UsersStats = selectFilteredUsersStats(state) || { + total_users_count: 0, + }; + totalCount = filteredUserStats.total_users_count; + } - const searchTerm = state.views.search.systemUsersSearch?.term || ''; let users = []; if (searchTerm) { - users = searchProfilesInTeam(state, teamId, searchTerm, false, {skipInactive: true}); + users = searchProfilesInTeam(state, teamId, searchTerm, false, {active: true, ...filters}); usersToAdd = searchUsersToAdd(usersToAdd, searchTerm); } else { - users = getProfilesInTeam(state, teamId, {skipInactive: true}); + users = getProfilesInTeam(state, teamId, {active: true, ...filters}); } return { + filters, teamId, team, users, teamMembers, usersToAdd, usersToRemove, - totalCount: stats.active_member_count, + totalCount, searchTerm, + enableGuestAccounts: config.EnableGuestAccounts === 'true', }; } function mapDispatchToProps(dispatch: Dispatch) { @@ -83,7 +107,9 @@ function mapDispatchToProps(dispatch: Dispatch) { getTeamStats: loadTeamStats, loadProfilesAndReloadTeamMembers, searchProfilesAndTeamMembers, - setSystemUsersSearch, + getFilteredUsersStats, + setUserGridSearch, + setUserGridFilters, }, dispatch), }; } diff --git a/components/admin_console/team_channel_settings/team/details/team_members/team_members.test.tsx b/components/admin_console/team_channel_settings/team/details/team_members/team_members.test.tsx index 7d18217e0887..761d4315b649 100644 --- a/components/admin_console/team_channel_settings/team/details/team_members/team_members.test.tsx +++ b/components/admin_console/team_channel_settings/team/details/team_members/team_members.test.tsx @@ -20,6 +20,7 @@ describe('admin_console/team_channel_settings/team/TeamMembers', () => { const team: Team = Object.assign(TestHelper.getTeamMock({id: 'team-1'})); const baseProps = { + filters: {}, teamId: 'team-1', team, users: [user1, user2, user3], @@ -30,6 +31,7 @@ describe('admin_console/team_channel_settings/team/TeamMembers', () => { [user2.id]: membership2, [user3.id]: membership3, }, + enableGuestAccounts: true, totalCount: 3, loading: false, @@ -42,7 +44,9 @@ describe('admin_console/team_channel_settings/team/TeamMembers', () => { getTeamStats: jest.fn(), loadProfilesAndReloadTeamMembers: jest.fn(), searchProfilesAndTeamMembers: jest.fn(), - setSystemUsersSearch: jest.fn(), + getFilteredUsersStats: jest.fn(), + setUserGridSearch: jest.fn(), + setUserGridFilters: jest.fn(), }, }; diff --git a/components/admin_console/team_channel_settings/team/details/team_members/team_members.tsx b/components/admin_console/team_channel_settings/team/details/team_members/team_members.tsx index 10cea21faa6c..c7d57236f30c 100644 --- a/components/admin_console/team_channel_settings/team/details/team_members/team_members.tsx +++ b/components/admin_console/team_channel_settings/team/details/team_members/team_members.tsx @@ -4,9 +4,11 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import {Dictionary} from 'mattermost-redux/types/utilities'; -import {UserProfile} from 'mattermost-redux/types/users'; +import {ServerError} from 'mattermost-redux/types/errors'; +import {UserProfile, UsersStats, GetFilteredUsersStatsOpts} from 'mattermost-redux/types/users'; import {TeamMembership, Team} from 'mattermost-redux/types/teams'; +import {Dictionary} from 'mattermost-redux/types/utilities'; +import GeneralConstants from 'mattermost-redux/constants/general'; import {t} from 'utils/i18n'; import Constants from 'utils/constants'; @@ -16,10 +18,13 @@ import UserGrid from 'components/admin_console/user_grid/user_grid'; import {BaseMembership} from 'components/admin_console/user_grid/user_grid_role_dropdown'; import AddUsersToTeamModal from 'components/add_users_to_team_modal'; import ToggleModalButton from 'components/toggle_modal_button'; +import {FilterOptions} from 'components/admin_console/filter/filter'; type Props = { teamId: string; team: Team; + filters: GetFilteredUsersStatsOpts; + users: UserProfile[]; usersToRemove: Dictionary; usersToAdd: Dictionary; @@ -28,6 +33,7 @@ type Props = { totalCount: number; searchTerm: string; loading?: boolean; + enableGuestAccounts: boolean; onAddCallback: (users: UserProfile[]) => void; onRemoveCallback: (user: UserProfile) => void; @@ -43,7 +49,14 @@ type Props = { searchProfilesAndTeamMembers: (term: string, options?: {}) => Promise<{ data: boolean; }>; - setSystemUsersSearch: (term: string) => Promise<{ + getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ + data?: UsersStats; + error?: ServerError; + }>; + setUserGridSearch: (term: string) => Promise<{ + data: boolean; + }>; + setUserGridFilters: (filters: GetFilteredUsersStatsOpts) => Promise<{ data: boolean; }>; }; @@ -70,29 +83,36 @@ export default class TeamMembers extends React.PureComponent { public componentDidMount() { const {teamId} = this.props; - const {loadProfilesAndReloadTeamMembers, getTeamStats, setSystemUsersSearch} = this.props.actions; + const {loadProfilesAndReloadTeamMembers, getTeamStats, setUserGridSearch, setUserGridFilters} = this.props.actions; Promise.all([ - setSystemUsersSearch(''), + setUserGridSearch(''), + setUserGridFilters({}), getTeamStats(teamId), - loadProfilesAndReloadTeamMembers(0, PROFILE_CHUNK_SIZE * 2, teamId), + loadProfilesAndReloadTeamMembers(0, PROFILE_CHUNK_SIZE * 2, teamId, {active: true}), ]).then(() => this.setStateLoading(false)); } - public componentDidUpdate(prevProps: Props) { - if (prevProps.searchTerm !== this.props.searchTerm) { + public async componentDidUpdate(prevProps: Props) { + const filtersModified = JSON.stringify(prevProps.filters) !== JSON.stringify(this.props.filters); + const searchTermModified = prevProps.searchTerm !== this.props.searchTerm; + if (filtersModified || searchTermModified) { this.setStateLoading(true); clearTimeout(this.searchTimeoutId); const searchTerm = this.props.searchTerm; + const filters = this.props.filters; if (searchTerm === '') { this.searchTimeoutId = 0; + if (filtersModified) { + await prevProps.actions.loadProfilesAndReloadTeamMembers(0, PROFILE_CHUNK_SIZE * 2, prevProps.teamId, {active: true, ...filters}); + } this.setStateLoading(false); return; } const searchTimeoutId = window.setTimeout( async () => { - await prevProps.actions.searchProfilesAndTeamMembers(searchTerm, {team_id: this.props.teamId, allow_inactive: false}); + await prevProps.actions.searchProfilesAndTeamMembers(searchTerm, {...filters, team_id: this.props.teamId, allow_inactive: false}); if (searchTimeoutId !== this.searchTimeoutId) { return; @@ -112,8 +132,8 @@ export default class TeamMembers extends React.PureComponent { private loadPage = async (page: number) => { const {loadProfilesAndReloadTeamMembers} = this.props.actions; - const {teamId} = this.props; - await loadProfilesAndReloadTeamMembers(page + 1, PROFILE_CHUNK_SIZE, teamId); + const {teamId, filters} = this.props; + await loadProfilesAndReloadTeamMembers(page + 1, PROFILE_CHUNK_SIZE, teamId, {active: true, ...filters}); } private removeUser = (user: UserProfile) => { @@ -125,7 +145,36 @@ export default class TeamMembers extends React.PureComponent { } private search = async (term: string) => { - this.props.actions.setSystemUsersSearch(term); + this.props.actions.setUserGridSearch(term); + } + + private onFilter = async (filterOptions: FilterOptions) => { + const roles = filterOptions.role.values; + const systemRoles: string[] = []; + const teamRoles: string[] = []; + let filters = {}; + Object.keys(roles).forEach((filterKey: string) => { + if (roles[filterKey].value) { + if (filterKey.includes('team')) { + teamRoles.push(filterKey); + } else { + systemRoles.push(filterKey); + } + } + }); + + if (systemRoles.length > 0 || teamRoles.length > 0) { + if (systemRoles.length > 0) { + filters = {roles: systemRoles}; + } + if (teamRoles.length > 0) { + filters = {...filters, team_roles: teamRoles}; + } + this.props.actions.setUserGridFilters({roles: systemRoles, team_roles: teamRoles}); + this.props.actions.getFilteredUsersStats({in_team: this.props.teamId, include_bots: true, ...filters}); + } else { + this.props.actions.setUserGridFilters(filters); + } } private updateMembership = (membership: BaseMembership) => { @@ -134,6 +183,67 @@ export default class TeamMembers extends React.PureComponent { public render = () => { const {users, team, usersToAdd, usersToRemove, teamMembers, totalCount, searchTerm} = this.props; + + const filterOptions: FilterOptions = { + role: { + name: ( + + ), + values: { + [GeneralConstants.SYSTEM_GUEST_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.TEAM_USER_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.TEAM_ADMIN_ROLE]: { + name: ( + + ), + value: false, + }, + [GeneralConstants.SYSTEM_ADMIN_ROLE]: { + name: ( + + ), + value: false, + }, + }, + keys: [GeneralConstants.SYSTEM_GUEST_ROLE, GeneralConstants.TEAM_USER_ROLE, GeneralConstants.TEAM_ADMIN_ROLE, GeneralConstants.SYSTEM_ADMIN_ROLE], + }, + }; + if (!this.props.enableGuestAccounts) { + delete filterOptions.role.values[GeneralConstants.SYSTEM_GUEST_ROLE]; + filterOptions.role.keys = [GeneralConstants.TEAM_USER_ROLE, GeneralConstants.TEAM_ADMIN_ROLE, GeneralConstants.SYSTEM_ADMIN_ROLE]; + } + const filterKeys = ['role']; + const filterProps = { + options: filterOptions, + keys: filterKeys, + onFilter: this.onFilter, + }; + return ( { includeUsers={usersToAdd} excludeUsers={usersToRemove} scope={'team'} + filterProps={filterProps} /> ); diff --git a/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap b/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap index 752fa93ac00e..e95b8a71c800 100644 --- a/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap +++ b/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap @@ -37,6 +37,13 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with ] } endCount={2} + filterProps={ + Object { + "keys": Array [], + "onFilter": [Function], + "options": Object {}, + } + } loading={false} nextPage={[Function]} page={0} @@ -443,6 +450,13 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with ] } endCount={3} + filterProps={ + Object { + "keys": Array [], + "onFilter": [Function], + "options": Object {}, + } + } loading={false} nextPage={[Function]} page={0} @@ -1015,6 +1029,13 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with ] } endCount={1} + filterProps={ + Object { + "keys": Array [], + "onFilter": [Function], + "options": Object {}, + } + } loading={false} nextPage={[Function]} page={0} diff --git a/components/admin_console/user_grid/user_grid.test.tsx b/components/admin_console/user_grid/user_grid.test.tsx index e4c35f47d62a..b9b69b0c1898 100644 --- a/components/admin_console/user_grid/user_grid.test.tsx +++ b/components/admin_console/user_grid/user_grid.test.tsx @@ -55,6 +55,12 @@ describe('components/admin_console/user_grid/UserGrid', () => { totalCount: 2, loading: false, term: '', + + filterProps: { + options: {}, + keys: [], + onFilter: jest.fn(), + } }; test('should match snapshot with 2 users', () => { diff --git a/components/admin_console/user_grid/user_grid.tsx b/components/admin_console/user_grid/user_grid.tsx index 1f70d0dae6c5..930f2d4e5a8e 100644 --- a/components/admin_console/user_grid/user_grid.tsx +++ b/components/admin_console/user_grid/user_grid.tsx @@ -9,6 +9,7 @@ import {TeamMembership} from 'mattermost-redux/types/teams'; import {ChannelMembership} from 'mattermost-redux/types/channels'; import Badge from 'components/widgets/badges/badge'; +import {FilterOptions} from 'components/admin_console/filter/filter'; import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid'; import UserGridName from './user_grid_name'; @@ -33,13 +34,19 @@ type Props = { totalCount: number; loading: boolean; term: string; -} + + filterProps: { + options: FilterOptions; + keys: string[]; + onFilter: (options: FilterOptions) => void; + }; +}; type State = { loading: boolean; page: number; membershipsToUpdate: { [userId: string]: BaseMembership | TeamMembership | ChannelMembership }; -} +}; const USERS_PER_PAGE = 10; const ROW_HEIGHT = 80; @@ -76,6 +83,11 @@ export default class UserGrid extends React.PureComponent { this.setState({page: 0}); } + private onFilter = async (filters: FilterOptions) => { + this.props.filterProps?.onFilter(filters); + this.setState({page: 0}); + } + private getVisibleTotalCount = (): number => { const {includeUsers, excludeUsers, totalCount} = this.props; const includeUsersCount = Object.keys(includeUsers).length; @@ -285,6 +297,7 @@ export default class UserGrid extends React.PureComponent { term={this.props.term || ''} placeholderEmpty={placeholderEmpty} rowsContainerStyles={rowsContainerStyles} + filterProps={{...this.props.filterProps, onFilter: this.onFilter}} /> ); } diff --git a/components/textbox/index.ts b/components/textbox/index.ts index 67f4736a588c..8d486f6c8106 100644 --- a/components/textbox/index.ts +++ b/components/textbox/index.ts @@ -33,8 +33,8 @@ const makeMapStateToProps = () => { return { currentUserId: getCurrentUserId(state), currentTeamId: teamId, - profilesInChannel: getProfilesInChannel(state, ownProps.channelId, true), - profilesNotInChannel: getProfilesNotInChannel(state, ownProps.channelId, true), + profilesInChannel: getProfilesInChannel(state, ownProps.channelId, {active: true}), + profilesNotInChannel: getProfilesNotInChannel(state, ownProps.channelId, {active: true}), autocompleteGroups: getAssociatedGroupsForReference(state, teamId, ownProps.channelId) }; }; diff --git a/components/widgets/admin_console/admin_panel.scss b/components/widgets/admin_console/admin_panel.scss index a3d14942aac3..e3db18213b76 100644 --- a/components/widgets/admin_console/admin_panel.scss +++ b/components/widgets/admin_console/admin_panel.scss @@ -6,7 +6,7 @@ .AdminPanel { @include border-radius($border-rad); @include box-shadow(0 1px 2px 0 alpha-color($black, .5)); - @include clearfix; + font-size: .95em; margin: 2em 0 1em; padding: 0; diff --git a/components/widgets/admin_console/admin_panel.test.tsx b/components/widgets/admin_console/admin_panel.test.tsx index 39530fe9c9fc..de40c8281a64 100644 --- a/components/widgets/admin_console/admin_panel.test.tsx +++ b/components/widgets/admin_console/admin_panel.test.tsx @@ -21,7 +21,7 @@ describe('components/widgets/admin_console/AdminPanel', () => { const wrapper = shallow({'Test'}); expect(wrapper).toMatchInlineSnapshot(`
{ ); expect(wrapper).toMatchInlineSnapshot(`
{ ); expect(wrapper).toMatchInlineSnapshot(`
{ `, ); }); -}); +}); \ No newline at end of file diff --git a/components/widgets/admin_console/admin_panel.tsx b/components/widgets/admin_console/admin_panel.tsx index 07aa80ded207..17243cf52ba6 100644 --- a/components/widgets/admin_console/admin_panel.tsx +++ b/components/widgets/admin_console/admin_panel.tsx @@ -23,7 +23,7 @@ type Props = { const AdminPanel: React.FC = (props: Props) => (
{ const initialState = { modalSearch: '', systemUsersSearch: {}, + userGridSearch: {}, }; test('Initial state', () => { @@ -38,4 +39,39 @@ describe('Reducers.Search', () => { modalSearch: 'something', }); }); + + test('should set user grid search', () => { + const filters = {team_id: '123456789'}; + const nextState = searchReducer( + { + userGridSearch: {filters}, + }, + { + type: SearchTypes.SET_USER_GRID_SEARCH, + data: 'something', + }, + ); + + expect(nextState).toEqual({ + ...initialState, + userGridSearch: {term: 'something', filters}, + }); + }); + + test('should set user grid filters', () => { + const nextState = searchReducer( + { + userGridSearch: {term: 'something', filters: {team_id: '123456789'}}, + }, + { + type: SearchTypes.SET_USER_GRID_FILTERS, + data: {team_id: '1', channel_roles: ['channel_admin']}, + }, + ); + + expect(nextState).toEqual({ + ...initialState, + userGridSearch: {term: 'something', filters: {team_id: '1', channel_roles: ['channel_admin']}}, + }); + }); }); diff --git a/sass/admin_console_base/_module.scss b/sass/admin_console_base/_module.scss new file mode 100644 index 000000000000..deec3e22bd22 --- /dev/null +++ b/sass/admin_console_base/_module.scss @@ -0,0 +1,2 @@ +// Only for combining all the files in this folder +@import 'sys_css_variables'; diff --git a/sass/admin_console_base/_sys_css_variables.scss b/sass/admin_console_base/_sys_css_variables.scss new file mode 100644 index 000000000000..4ddfcbb078ed --- /dev/null +++ b/sass/admin_console_base/_sys_css_variables.scss @@ -0,0 +1,51 @@ +:root { + // Cloned default hex values from base/css_variables to be used in the system console + --sys-away-indicator: #ffbc42; + --sys-button-bg: #166de0; + --sys-button-color: #ffffff; + --sys-center-channel-bg: #ffffff; + --sys-center-channel-color: #3d3c40; + --sys-dnd-indicator: #f74343; + --sys-error-text: #fd5960; + --sys-link-color: #2389d7; + --sys-mention-bg: #ffffff; + --sys-mention-color: #145dbf; + --sys-mention-highlight-bg: #ffe577; + --sys-mention-highlight-link: #166de0; + --sys-new-message-separator: #ff8800; + --sys-online-indicator: #06d6a0; + --sys-sidebar-bg: #145dbf; + --sys-sidebar-header-bg: #1153ab; + --sys-sidebar-header-text-color: #ffffff; + --sys-sidebar-text: #ffffff; + --sys-sidebar-text-60: #ffffff99; + --sys-sidebar-text-80: #ffffffcc; + --sys-sidebar-text-active-border: #579eff; + --sys-sidebar-text-active-color: #ffffff; + --sys-sidebar-text-hover-bg: #4578bf; + --sys-sidebar-unread-text: #ffffff; + + // Cloned RGB values from base/css_variables, to be used as rgba(var(--sys-color), opacity_here) to apply opacity to base colors + --sys-away-indicator-rgb: 255, 188, 66; + --sys-button-bg-rgb: 22, 109, 224; + --sys-button-color-rgb: 255, 255, 255; + --sys-center-channel-bg-rgb: 255, 255, 255; + --sys-center-channel-color-rgb: 61, 60, 64; + --sys-dnd-indicator-rgb: 247, 67, 67; + --sys-error-text-color-rgb: 253, 89, 96; + --sys-link-color-rgb: 35, 137, 215; + --sys-mention-bg-rgb: 255, 255, 255; + --sys-mention-color-rgb: 20, 93, 191; + --sys-mention-highlight-bg-rgb: 255, 229, 119; + --sys-mention-highlight-link-rgb: 22, 109, 224; + --sys-new-message-separator-rgb: 255, 136, 0; + --sys-online-indicator-rgb: 6, 214, 160; + --sys-sidebar-bg-rgb: 20, 93, 191; + --sys-sidebar-header-bg-rgb: 17, 83, 171; + --sys-sidebar-header-text-color-rgb: 255, 255, 255; + --sys-sidebar-text-rgb: 255, 255, 255; + --sys-sidebar-text-active-border-rgb: 87, 158, 255; + --sys-sidebar-text-active-color-rgb: 255, 255, 255; + --sys-sidebar-text-hover-bg-rgb: 69, 120, 191; + --sys-sidebar-unread-text-rgb: 255, 255, 255; +} diff --git a/sass/styles.scss b/sass/styles.scss index b44b744f196e..c1c6e602a373 100644 --- a/sass/styles.scss +++ b/sass/styles.scss @@ -18,3 +18,4 @@ @import 'responsive/module'; @import 'widgets/module'; @import 'responsive/module'; +@import 'admin_console_base/module' diff --git a/types/store/index.ts b/types/store/index.ts index 48631f48b2c4..b3fd04ebe6a8 100644 --- a/types/store/index.ts +++ b/types/store/index.ts @@ -96,6 +96,14 @@ export type GlobalState = BaseGlobalState & { team: string; filter: string; }; + userGridSearch: { + term: string; + filters: { + roles?: string[]; + channel_roles?: string[]; + team_roles?: string[]; + }; + }; }; notice: { diff --git a/utils/constants.jsx b/utils/constants.jsx index e6134165ac7e..7db9fed28713 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -449,6 +449,8 @@ export const UserFilters = { export const SearchTypes = keyMirror({ SET_MODAL_SEARCH: null, SET_SYSTEM_USERS_SEARCH: null, + SET_USER_GRID_SEARCH: null, + SET_USER_GRID_FILTERS: null, }); export const StorageTypes = keyMirror({